Commit f47e86fe authored by Phil Hughes's avatar Phil Hughes

Merge branch '30117-update-looks-job-log' into 'master'

Update looks job log

Closes #30117

See merge request !11663
parents 763a3acd cd023d42
......@@ -2,15 +2,11 @@
consistent-return, prefer-rest-params */
/* global Breakpoints */
import _ from 'underscore';
import { bytesToKiB } from './lib/utils/number_utils';
const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; };
const AUTO_SCROLL_OFFSET = 75;
const DOWN_BUILD_TRACE = '#down-build-trace';
window.Build = (function () {
Build.timeout = null;
Build.state = null;
function Build(options) {
......@@ -23,21 +19,22 @@ window.Build = (function () {
this.buildStage = this.options.buildStage;
this.$document = $(document);
this.logBytes = 0;
this.scrollOffsetPadding = 30;
this.updateDropdown = bind(this.updateDropdown, this);
this.updateDropdown = this.updateDropdown.bind(this);
this.getBuildTrace = this.getBuildTrace.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this);
this.$body = $('body');
this.$buildTrace = $('#build-trace');
this.$autoScrollContainer = $('.autoscroll-container');
this.$autoScrollStatus = $('#autoscroll-status');
this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
this.$upBuildTrace = $('#up-build-trace');
this.$downBuildTrace = $(DOWN_BUILD_TRACE);
this.$scrollTopBtn = $('#scroll-top');
this.$scrollBottomBtn = $('#scroll-bottom');
this.$buildRefreshAnimation = $('.js-build-refresh');
this.$buildScroll = $('#js-build-scroll');
this.$truncatedInfo = $('.js-truncated-info');
this.$buildTraceOutput = $('.js-build-output');
this.$scrollContainer = $('.js-scroll-container');
// Scroll controllers
this.$scrollTopBtn = $('.js-scroll-up');
this.$scrollBottomBtn = $('.js-scroll-down');
clearTimeout(Build.timeout);
// Init breakpoint checker
......@@ -56,54 +53,149 @@ window.Build = (function () {
.off('click', '.stage-item')
.on('click', '.stage-item', this.updateDropdown);
this.$document.on('scroll', this.initScrollMonitor.bind(this));
// add event listeners to the scroll buttons
this.$scrollTopBtn
.off('click')
.on('click', this.scrollToTop.bind(this));
this.$scrollBottomBtn
.off('click')
.on('click', this.scrollToBottom.bind(this));
$(window)
.off('resize.build')
.on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll)
.off('click.stepTrace')
.on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
this.initScrollButtonAffix();
this.invokeBuildTrace();
// eslint-disable-next-line
this.getBuildTrace()
.then(() => this.makeTraceScrollable())
.then(() => this.scrollToBottom());
this.verifyTopPosition();
}
Build.prototype.makeTraceScrollable = function () {
this.$scrollContainer.niceScroll({
cursorcolor: '#fff',
cursoropacitymin: 1,
cursorwidth: '3px',
railpadding: { top: 5, bottom: 5, right: 5 },
});
this.$scrollContainer.on('scroll', _.throttle(this.toggleScroll.bind(this), 100));
this.toggleScroll();
};
Build.prototype.canScroll = function () {
return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height();
};
/**
* | | Up | Down |
* |--------------------------|----------|----------|
* | on scroll bottom | active | disabled |
* | on scroll top | disabled | active |
* | no scroll | disabled | disabled |
* | on.('scroll') is on top | disabled | active |
* | on('scroll) is on bottom | active | disabled |
*
*/
Build.prototype.toggleScroll = function () {
const bottomScroll = this.$scrollContainer.scrollTop() +
this.scrollOffsetPadding +
this.$scrollContainer.height();
if (this.canScroll()) {
if (this.$scrollContainer.scrollTop() === 0) {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) {
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, true);
} else {
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, false);
}
}
};
Build.prototype.scrollToTop = function () {
this.$scrollContainer.getNiceScroll(0).doScrollTop(0);
this.toggleScroll();
};
Build.prototype.scrollToBottom = function () {
this.$scrollContainer.getNiceScroll(0).doScrollTo(this.$scrollContainer.prop('scrollHeight'));
this.toggleScroll();
};
Build.prototype.toggleDisableButton = function ($button, disable) {
if (disable && $button.prop('disabled')) return;
$button.prop('disabled', disable);
};
Build.prototype.toggleScrollAnimation = function (toggle) {
this.$scrollBottomBtn.toggleClass('animate', toggle);
};
/**
* Build trace top position depends on the space ocupied by the elments rendered before
*/
Build.prototype.verifyTopPosition = function () {
const $buildPage = $('.build-page');
const $header = $('.build-header', $buildPage);
const $runnersStuck = $('.js-build-stuck', $buildPage);
const $startsEnvironment = $('.js-environment-container', $buildPage);
const $erased = $('.js-build-erased', $buildPage);
let topPostion = 168;
if ($header) {
topPostion += $header.outerHeight();
}
if ($runnersStuck) {
topPostion += $runnersStuck.outerHeight();
}
if ($startsEnvironment) {
topPostion += $startsEnvironment.outerHeight();
}
if ($erased) {
topPostion += $erased.outerHeight() + 10;
}
this.$buildTrace.css({
top: topPostion,
});
};
Build.prototype.initSidebar = function () {
this.$sidebar = $('.js-build-sidebar');
this.$sidebar.niceScroll();
this.$document
.off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
};
Build.prototype.invokeBuildTrace = function () {
return this.getBuildTrace();
};
Build.prototype.getBuildTrace = function () {
return $.ajax({
url: `${this.pageUrl}/trace.json`,
dataType: 'json',
data: {
state: this.state,
},
success: ((log) => {
const $buildContainer = $('.js-build-output');
data: this.state,
})
.done((log) => {
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (log.state) {
this.state = log.state;
}
if (log.append) {
$buildContainer.append(log.html);
this.$buildTraceOutput.append(log.html);
this.logBytes += log.size;
} else {
$buildContainer.html(log.html);
this.$buildTraceOutput.html(log.html);
this.logBytes = log.size;
}
......@@ -114,141 +206,30 @@ window.Build = (function () {
const size = bytesToKiB(this.logBytes);
$('.js-truncated-info-size').html(`${size}`);
this.$truncatedInfo.removeClass('hidden');
this.initAffixTruncatedInfo();
} else {
this.$truncatedInfo.addClass('hidden');
}
this.checkAutoscroll();
if (!log.complete) {
this.toggleScrollAnimation(true);
Build.timeout = setTimeout(() => {
this.invokeBuildTrace();
//eslint-disable-next-line
this.getBuildTrace()
.then(() => this.scrollToBottom());
}, 4000);
} else {
this.$buildRefreshAnimation.remove();
this.toggleScrollAnimation(false);
}
if (log.status !== this.buildStatus) {
let pageUrl = this.pageUrl;
if (this.$autoScrollStatus.data('state') === 'enabled') {
pageUrl += DOWN_BUILD_TRACE;
}
gl.utils.visitUrl(pageUrl);
gl.utils.visitUrl(this.pageUrl);
}
}),
error: () => {
})
.fail(() => {
this.$buildRefreshAnimation.remove();
return this.initScrollMonitor();
},
});
};
Build.prototype.checkAutoscroll = function () {
if (this.$autoScrollStatus.data('state') === 'enabled') {
return $('html,body').scrollTop(this.$buildTrace.height());
}
// Handle a situation where user started new build
// but never scrolled a page
if (!this.$scrollTopBtn.is(':visible') &&
!this.$scrollBottomBtn.is(':visible') &&
!gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
this.$scrollBottomBtn.show();
}
};
Build.prototype.initScrollButtonAffix = function () {
// Hide everything initially
this.$scrollTopBtn.hide();
this.$scrollBottomBtn.hide();
this.$autoScrollContainer.hide();
};
// Page scroll listener to detect if user has scrolling page
// and handle following cases
// 1) User is at Top of Build Log;
// - Hide Top Arrow button
// - Show Bottom Arrow button
// - Disable Autoscroll and hide indicator (when build is running)
// 2) User is at Bottom of Build Log;
// - Show Top Arrow button
// - Hide Bottom Arrow button
// - Enable Autoscroll and show indicator (when build is running)
// 3) User is somewhere in middle of Build Log;
// - Show Top Arrow button
// - Show Bottom Arrow button
// - Disable Autoscroll and hide indicator (when build is running)
Build.prototype.initScrollMonitor = function () {
if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
!gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// User is somewhere in middle of Build Log
this.$scrollTopBtn.show();
if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
this.$scrollBottomBtn.show();
} else if (this.$buildRefreshAnimation.is(':visible') &&
!gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
this.$scrollBottomBtn.show();
} else {
this.$scrollBottomBtn.hide();
}
// Hide Autoscroll Status Indicator
if (this.$scrollBottomBtn.is(':visible')) {
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
} else {
this.$autoScrollContainer.css({
top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET,
}).show();
this.$autoScrollStatusText.addClass('animate');
}
} else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
!gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// User is at Top of Build Log
this.$scrollTopBtn.hide();
this.$scrollBottomBtn.show();
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
} else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
(this.$buildRefreshAnimation.is(':visible') &&
gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
// User is at Bottom of Build Log
this.$scrollTopBtn.show();
this.$scrollBottomBtn.hide();
// Show and Reposition Autoscroll Status Indicator
this.$autoScrollContainer.css({
top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET,
}).show();
this.$autoScrollStatusText.addClass('animate');
} else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) &&
gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// Build Log height is small
this.$scrollTopBtn.hide();
this.$scrollBottomBtn.hide();
// Hide Autoscroll Status Indicator
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
}
if (this.buildStatus === 'running' || this.buildStatus === 'pending') {
// Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
this.$autoScrollStatus.data(
'state',
gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled',
);
}
});
};
Build.prototype.shouldHideSidebarForViewport = function () {
......@@ -257,18 +238,23 @@ window.Build = (function () {
};
Build.prototype.toggleSidebar = function (shouldHide) {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
const shouldShow = !shouldHide;
this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
this.$buildTrace
.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
this.$truncatedInfo.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
this.$sidebar
.toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide);
};
Build.prototype.sidebarOnResize = function () {
this.toggleSidebar(this.shouldHideSidebarForViewport());
this.verifyTopPosition();
if (this.$scrollContainer.getNiceScroll(0)) {
this.toggleScroll();
}
};
Build.prototype.sidebarOnClick = function () {
......@@ -301,24 +287,5 @@ window.Build = (function () {
this.populateJobs(stage);
};
Build.prototype.stepTrace = function (e) {
e.preventDefault();
const $currentTarget = $(e.currentTarget);
$.scrollTo($currentTarget.attr('href'), {
offset: 0,
});
};
Build.prototype.initAffixTruncatedInfo = function () {
const offsetTop = this.$buildTrace.offset().top;
this.$truncatedInfo.affix({
offset: {
top: offsetTop,
},
});
};
return Build;
})();
......@@ -29,129 +29,140 @@
}
}
.build-page {
pre.trace {
background: $builds-trace-bg;
color: $white-light;
font-family: $monospace_font;
white-space: pre-wrap;
overflow: auto;
overflow-y: hidden;
font-size: 12px;
.fa-spinner {
font-size: 24px;
margin-left: 20px;
}
}
.environment-information {
background-color: $gray-light;
border: 1px solid $border-color;
padding: 12px $gl-padding;
border-radius: $border-radius-default;
@keyframes blinking-scroll-button {
0% { opacity: 0.2; }
25% { opacity: 0.5; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
svg {
position: relative;
top: 1px;
margin-right: 5px;
}
.build-page {
.sticky {
position: absolute;
left: 0;
right: 0;
}
.truncated-info {
text-align: center;
border-bottom: 1px solid;
background-color: $black;
height: 45px;
padding: 15px;
.build-trace-container {
position: absolute;
top: 225px;
left: 15px;
bottom: 10px;
background: $black;
color: $gray-darkest;
font-family: $monospace_font;
font-size: 12px;
&.affix {
top: 0;
&.sidebar-expanded {
right: 305px;
}
// with sidebar
&.affix.sidebar-expanded {
right: 312px;
left: 22px;
&.sidebar-collapsed {
right: 16px;
}
// without sidebar
&.affix.sidebar-collapsed {
right: 20px;
left: 20px;
code {
background: $black;
color: $gray-darkest;
}
&.affix-top {
position: absolute;
.top-bar {
top: 0;
margin: 0 auto;
right: 5px;
left: 5px;
}
height: 35px;
display: flex;
justify-content: flex-end;
border-bottom: 1px outset $white-light;
.truncated-info-size {
margin: 0 5px;
}
.truncated-info {
margin: 0 auto;
align-self: center;
.raw-link {
color: inherit;
margin-left: 5px;
text-decoration: underline;
.truncated-info-size {
margin: 0 5px;
}
.raw-link {
color: inherit;
margin-left: 5px;
text-decoration: underline;
}
}
}
}
}
.scroll-controls {
height: 100%;
.controllers {
display: flex;
align-self: center;
font-size: 15px;
.scroll-step {
width: 31px;
margin: 0 0 0 auto;
}
svg {
height: 15px;
display: block;
fill: $white-light;
}
.scroll-link,
.autoscroll-container {
right: 25px;
z-index: 1;
}
a,
.btn-scroll {
margin: 0 8px;
color: $white-light;
}
.scroll-link {
position: fixed;
display: block;
margin-bottom: 10px;
.btn-scroll.animate {
.first-triangle {
animation: blinking-scroll-button 1s ease infinite;
animation-delay: .3s;
}
&.scroll-top .gitlab-icon-scroll-up-hover,
&.scroll-top:hover .gitlab-icon-scroll-up,
&.scroll-bottom .gitlab-icon-scroll-down-hover,
&.scroll-bottom:hover .gitlab-icon-scroll-down {
display: none;
}
.second-triangle {
animation: blinking-scroll-button 1s ease infinite;
animation-delay: .2s;
}
&.scroll-top:hover .gitlab-icon-scroll-up-hover,
&.scroll-bottom:hover .gitlab-icon-scroll-down-hover {
display: inline-block;
}
.third-triangle {
animation: blinking-scroll-button 1s ease infinite;
}
&.scroll-top {
top: 10px;
}
&:disabled {
opacity: 1;
}
}
&.scroll-bottom {
bottom: -2px;
.btn-scroll:disabled {
opacity: 0.35;
cursor: not-allowed;
}
}
}
.autoscroll-container {
position: absolute;
.bash {
top: 35px;
left: 10px;
bottom: 0;
overflow-y: hidden;
padding-bottom: 20px;
padding-right: 20px;
}
&.sidebar-expanded {
.environment-information {
background-color: $gray-light;
border: 1px solid $border-color;
padding: 12px $gl-padding;
border-radius: $border-radius-default;
.scroll-link,
.autoscroll-container {
right: ($gutter_width + ($gl-padding * 2));
svg {
position: relative;
top: 1px;
margin-right: 5px;
}
}
.build-loader-animation {
position: relative;
width: 6px;
height: 6px;
margin: auto auto 12px 2px;
border-radius: 50%;
animation: blinking-dots 1s linear infinite;
}
}
.status-message {
......@@ -223,32 +234,6 @@
}
}
.build-trace {
background: $black;
color: $gray-darkest;
white-space: pre;
overflow-x: auto;
font-size: 12px;
position: relative;
.fa-spinner {
font-size: 24px;
}
.bash {
display: block;
}
.build-loader-animation {
position: relative;
width: 6px;
height: 6px;
margin: auto auto 12px 2px;
border-radius: 50%;
animation: blinking-dots 1s linear infinite;
}
}
.right-sidebar.build-sidebar {
padding: $gl-padding 0;
......
......@@ -68,15 +68,8 @@
- elsif @build.runner
\##{@build.runner.id}
.btn-group.btn-group-justified{ role: :group }
- if @build.has_trace?
= link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
- if @build.active?
= link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
- if can?(current_user, :update_build, @project) && @build.erasable?
= link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
class: "btn btn-sm btn-default", method: :post,
data: { confirm: "Are you sure you want to erase this build?" } do
Erase
- if @build.trigger_request
.build-widget
......
......@@ -8,7 +8,7 @@
- if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning
.bs-callout.bs-callout-warning.js-build-stuck
%p
- if no_runners_for_project?(@build.project)
This job is stuck, because the project doesn't have any runners online assigned to it.
......@@ -26,7 +26,7 @@
Runners page
- if @build.starts_environment?
.prepend-top-default
.prepend-top-default.js-environment-container
.environment-information
- if @build.outdated_deployment?
= ci_icon_for_status('success_with_warnings')
......@@ -47,39 +47,51 @@
- if environment.try(:last_deployment)
and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
.prepend-top-default
.prepend-top-default.js-build-erased
- if @build.erased?
.erased.alert.alert-warning
- if @build.erased_by_user?
Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls
.scroll-step
%a.scroll-link.scroll-top{ href: '#up-build-trace', id: 'scroll-top', title: 'Scroll to top' }
= custom_icon('scroll_up')
= custom_icon('scroll_up_hover_active')
%a.scroll-link.scroll-bottom{ href: '#down-build-trace', id: 'scroll-bottom', title: 'Scroll to bottom' }
= custom_icon('scroll_down')
= custom_icon('scroll_down_hover_active')
- if @build.active?
.autoscroll-container
%span.status-message#autoscroll-status{ data: { state: 'disabled' } }
%span.status-text Autoscroll active
%i.status-icon
= custom_icon('scroll_down_hover_active')
#up-build-trace
%pre.build-trace#build-trace
.prepend-top-default
.build-trace-container#build-trace
.top-bar.sticky
.js-truncated-info.truncated-info.hidden<
Showing last
%span.js-truncated-info-size.truncated-info-size><
KiB of log -
%a.js-raw-link.raw-link{ :href => raw_namespace_project_build_path(@project.namespace, @project, @build) }>< Complete Raw
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
%a.js-raw-link.raw-link{ href: raw_namespace_project_build_path(@project.namespace, @project, @build) }>< Complete Raw
.controllers
- if @build.has_trace?
= link_to raw_namespace_project_build_path(@project.namespace, @project, @build),
title: 'Open raw trace',
data: { placement: 'top', container: 'body' },
class: 'js-raw-link-controller has-tooltip' do
= icon('download')
- if can?(current_user, :update_build, @project) && @build.erasable?
= link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
title: 'Erase Build',
class: 'has-tooltip js-erase-link' do
= icon('trash')
#down-build-trace
%button.js-scroll-up.btn-scroll.btn-transparent.btn-blank.has-tooltip{ type: 'button',
disabled: true,
title: 'Scroll Up',
data: { placement: 'top', container: 'body'} }
= custom_icon('scroll_up')
%button.js-scroll-down.btn-scroll.btn-transparent.btn-blank.has-tooltip{ type: 'button',
disabled: true,
title: 'Scroll Down',
data: { placement: 'top', container: 'body'} }
= custom_icon('scroll_down')
.bash.sticky.js-scroll-container
%code.js-build-output
.build-loader-animation.js-build-refresh
= render "sidebar"
......
<svg width="16" height="33" class="gitlab-icon-scroll-down" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M1.385 5.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15V5.535a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
<svg width="12" height="16" viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path class="first-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043c.124 0 .23-.035.321-.105.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/>
<path class="second-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/>
<path class="third-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91A.458.458 0 0 1 6.257 6h-.37a.626.626 0 0 1-.136-.09"/>
</svg>
<svg width="16" height="33" class="gitlab-icon-scroll-down-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
</svg>
<svg width="16" height="33" class="gitlab-icon-scroll-up" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M1.385 14.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15v-12.47a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 2.609V6.96a.688.688 0 0 1-.69.688.687.687 0 0 1-.69-.688V2.627L6.155 3.972a.69.69 0 0 1-.976-.976L7.705.47a.685.685 0 0 1 .494-.2.685.685 0 0 1 .493.2l2.526 2.526a.69.69 0 1 1-.976.976L8.88 2.609zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
</svg>
<svg width="12" height="16" viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043c.124 0 .23.035.321.105.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105"/><path d="M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09"/><path d="M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09A.458.458 0 0 0 6.257 10h-.37a.626.626 0 0 0-.136.09"/></svg>
<svg width="16" height="33" class="gitlab-icon-scroll-up-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M8.88 2.646l1.362 1.362a.69.69 0 0 0 .976-.976L8.692.507A.685.685 0 0 0 8.2.306a.685.685 0 0 0-.494.2L5.179 3.033a.69.69 0 1 0 .976.976L7.5 2.663v4.179c0 .38.306.688.69.688.381 0 .69-.306.69-.688V2.646zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
</svg>
......@@ -190,7 +190,7 @@ feature 'Builds', :feature do
end
it do
expect(page).to have_link 'Raw'
expect(page).to have_css('.js-raw-link')
end
end
......@@ -369,14 +369,14 @@ feature 'Builds', :feature do
end
end
describe 'GET /:project/builds/:id/raw' do
describe 'GET /:project/builds/:id/raw', :js do
context 'access source' do
context 'build from project' do
before do
Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
build.run!
visit namespace_project_build_path(project.namespace, project, build)
page.within('.js-build-sidebar') { click_link 'Raw' }
find('.js-raw-link-controller').click()
end
it 'sends the right headers' do
......@@ -388,7 +388,7 @@ feature 'Builds', :feature do
context 'build from other project' do
before do
Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
build2.run!
visit raw_namespace_project_build_path(project.namespace, project, build2)
end
......@@ -403,7 +403,7 @@ feature 'Builds', :feature do
let(:existing_file) { Tempfile.new('existing-trace-file').path }
before do
Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
build.run!
......@@ -413,13 +413,13 @@ feature 'Builds', :feature do
visit namespace_project_build_path(project.namespace, project, build)
end
context 'when build has trace in file' do
context 'when build has trace in file', :js do
let(:paths) do
[existing_file]
end
before do
page.within('.js-build-sidebar') { click_link 'Raw' }
find('.js-raw-link-controller').click()
end
it 'sends the right headers' do
......@@ -433,7 +433,7 @@ feature 'Builds', :feature do
let(:paths) { [] }
it 'sends the right headers' do
expect(page.status_code).not_to have_link('Raw')
expect(page.status_code).not_to have_selector('.js-raw-link-controller')
end
end
end
......
......@@ -14,7 +14,6 @@ describe('Build', () => {
beforeEach(() => {
loadFixtures('builds/build-with-artifacts.html.raw');
spyOn($, 'ajax');
});
describe('class constructor', () => {
......@@ -33,7 +32,6 @@ describe('Build', () => {
it('copies build options', function () {
expect(this.build.pageUrl).toBe(BUILD_URL);
expect(this.build.buildUrl).toBe(`${BUILD_URL}.json`);
expect(this.build.buildStatus).toBe('success');
expect(this.build.buildStage).toBe('test');
expect(this.build.state).toBe('');
......@@ -65,27 +63,14 @@ describe('Build', () => {
});
describe('running build', () => {
beforeEach(function () {
this.build = new Build();
});
it('updates the build trace on an interval', function () {
const deferred1 = $.Deferred();
const deferred2 = $.Deferred();
const deferred3 = $.Deferred();
spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001);
expect($.ajax.calls.count()).toBe(1);
// We have to do it this way to prevent Webpack to fail to compile
// when destructuring assignments and reusing
// the same variables names inside the same scope
let args = $.ajax.calls.argsFor(0)[0];
expect(args.url).toBe(`${BUILD_URL}/trace.json`);
expect(args.dataType).toBe('json');
expect(args.success).toEqual(jasmine.any(Function));
args.success.call($, {
deferred1.resolve({
html: '<span>Update<span>',
status: 'running',
state: 'newstate',
......@@ -93,20 +78,9 @@ describe('Build', () => {
complete: false,
});
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
expect(this.build.state).toBe('newstate');
jasmine.clock().tick(4001);
expect($.ajax.calls.count()).toBe(3);
args = $.ajax.calls.argsFor(2)[0];
expect(args.url).toBe(`${BUILD_URL}/trace.json`);
expect(args.dataType).toBe('json');
expect(args.data.state).toBe('newstate');
expect(args.success).toEqual(jasmine.any(Function));
deferred2.resolve();
args.success.call($, {
deferred3.resolve({
html: '<span>More</span>',
status: 'running',
state: 'finalstate',
......@@ -114,150 +88,222 @@ describe('Build', () => {
complete: true,
});
this.build = new Build();
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
expect(this.build.state).toBe('newstate');
jasmine.clock().tick(4001);
expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
expect(this.build.state).toBe('finalstate');
});
it('replaces the entire build trace', () => {
const deferred1 = $.Deferred();
const deferred2 = $.Deferred();
const deferred3 = $.Deferred();
spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001);
let args = $.ajax.calls.argsFor(0)[0];
args.success.call($, {
html: '<span>Update</span>',
deferred1.resolve({
html: '<span>Update<span>',
status: 'running',
append: false,
complete: false,
});
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
deferred2.resolve();
jasmine.clock().tick(4001);
args = $.ajax.calls.argsFor(2)[0];
args.success.call($, {
deferred3.resolve({
html: '<span>Different</span>',
status: 'running',
append: false,
});
this.build = new Build();
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
jasmine.clock().tick(4001);
expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
});
it('reloads the page when the build is done', () => {
spyOn(gl.utils, 'visitUrl');
const deferred = $.Deferred();
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0);
success.call($, {
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
html: '<span>Final</span>',
status: 'passed',
append: true,
complete: true,
});
this.build = new Build();
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
});
});
describe('truncated information', () => {
describe('when size is less than total', () => {
it('shows information about truncated log', () => {
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0);
success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
});
expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
describe('truncated information', () => {
describe('when size is less than total', () => {
it('shows information about truncated log', () => {
spyOn(gl.utils, 'visitUrl');
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
});
it('shows the size in KiB', () => {
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0);
const size = 50;
success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
size,
total: 100,
});
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${bytesToKiB(size)}`);
this.build = new Build();
expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
});
it('shows the size in KiB', () => {
const size = 50;
spyOn(gl.utils, 'visitUrl');
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
html: '<span>Update</span>',
status: 'success',
append: false,
size,
total: 100,
});
it('shows incremented size', () => {
jasmine.clock().tick(4001);
let args = $.ajax.calls.argsFor(0)[0];
args.success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
});
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${bytesToKiB(50)}`);
jasmine.clock().tick(4001);
args = $.ajax.calls.argsFor(2)[0];
args.success.call($, {
html: '<span>Update</span>',
status: 'success',
append: true,
size: 10,
total: 100,
});
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${bytesToKiB(60)}`);
this.build = new Build();
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${bytesToKiB(size)}`);
});
it('shows incremented size', () => {
const deferred1 = $.Deferred();
const deferred2 = $.Deferred();
const deferred3 = $.Deferred();
spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
spyOn(gl.utils, 'visitUrl');
deferred1.resolve({
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
});
it('renders the raw link', () => {
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0);
success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
});
expect(
document.querySelector('.js-raw-link').textContent.trim(),
).toContain('Complete Raw');
deferred2.resolve();
this.build = new Build();
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${bytesToKiB(50)}`);
jasmine.clock().tick(4001);
deferred3.resolve({
html: '<span>Update</span>',
status: 'success',
append: true,
size: 10,
total: 100,
});
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${bytesToKiB(60)}`);
});
describe('when size is equal than total', () => {
it('does not show the trunctated information', () => {
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0);
it('renders the raw link', () => {
const deferred = $.Deferred();
spyOn(gl.utils, 'visitUrl');
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
});
success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 100,
total: 100,
});
this.build = new Build();
expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
expect(
document.querySelector('.js-raw-link').textContent.trim(),
).toContain('Complete Raw');
});
});
describe('when size is equal than total', () => {
it('does not show the trunctated information', () => {
const deferred = $.Deferred();
spyOn(gl.utils, 'visitUrl');
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
html: '<span>Update</span>',
status: 'success',
append: false,
size: 100,
total: 100,
});
this.build = new Build();
expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
});
});
});
describe('output trace', () => {
beforeEach(() => {
const deferred = $.Deferred();
spyOn(gl.utils, 'visitUrl');
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
});
this.build = new Build();
});
it('should render trace controls', () => {
const controllers = document.querySelector('.controllers');
expect(controllers.querySelector('.js-raw-link-controller')).toBeDefined();
expect(controllers.querySelector('.js-erase-link')).toBeDefined();
expect(controllers.querySelector('.js-scroll-up')).toBeDefined();
expect(controllers.querySelector('.js-scroll-down')).toBeDefined();
});
it('should render received output', () => {
expect(
document.querySelector('.js-build-output').innerHTML,
).toEqual('<span>Update</span>');
});
});
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment