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 @@ ...@@ -2,15 +2,11 @@
consistent-return, prefer-rest-params */ consistent-return, prefer-rest-params */
/* global Breakpoints */ /* global Breakpoints */
import _ from 'underscore';
import { bytesToKiB } from './lib/utils/number_utils'; 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 () { window.Build = (function () {
Build.timeout = null; Build.timeout = null;
Build.state = null; Build.state = null;
function Build(options) { function Build(options) {
...@@ -23,21 +19,22 @@ window.Build = (function () { ...@@ -23,21 +19,22 @@ window.Build = (function () {
this.buildStage = this.options.buildStage; this.buildStage = this.options.buildStage;
this.$document = $(document); this.$document = $(document);
this.logBytes = 0; 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.$body = $('body');
this.$buildTrace = $('#build-trace'); 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.$buildRefreshAnimation = $('.js-build-refresh');
this.$buildScroll = $('#js-build-scroll');
this.$truncatedInfo = $('.js-truncated-info'); 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); clearTimeout(Build.timeout);
// Init breakpoint checker // Init breakpoint checker
...@@ -56,54 +53,149 @@ window.Build = (function () { ...@@ -56,54 +53,149 @@ window.Build = (function () {
.off('click', '.stage-item') .off('click', '.stage-item')
.on('click', '.stage-item', this.updateDropdown); .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) $(window)
.off('resize.build') .off('resize.build')
.on('resize.build', this.sidebarOnResize.bind(this)); .on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll)
.off('click.stepTrace')
.on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate(); 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 () { Build.prototype.initSidebar = function () {
this.$sidebar = $('.js-build-sidebar'); this.$sidebar = $('.js-build-sidebar');
this.$sidebar.niceScroll(); 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 () { Build.prototype.getBuildTrace = function () {
return $.ajax({ return $.ajax({
url: `${this.pageUrl}/trace.json`, url: `${this.pageUrl}/trace.json`,
dataType: 'json', data: this.state,
data: { })
state: this.state, .done((log) => {
},
success: ((log) => {
const $buildContainer = $('.js-build-output');
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (log.state) { if (log.state) {
this.state = log.state; this.state = log.state;
} }
if (log.append) { if (log.append) {
$buildContainer.append(log.html); this.$buildTraceOutput.append(log.html);
this.logBytes += log.size; this.logBytes += log.size;
} else { } else {
$buildContainer.html(log.html); this.$buildTraceOutput.html(log.html);
this.logBytes = log.size; this.logBytes = log.size;
} }
...@@ -114,141 +206,30 @@ window.Build = (function () { ...@@ -114,141 +206,30 @@ window.Build = (function () {
const size = bytesToKiB(this.logBytes); const size = bytesToKiB(this.logBytes);
$('.js-truncated-info-size').html(`${size}`); $('.js-truncated-info-size').html(`${size}`);
this.$truncatedInfo.removeClass('hidden'); this.$truncatedInfo.removeClass('hidden');
this.initAffixTruncatedInfo();
} else { } else {
this.$truncatedInfo.addClass('hidden'); this.$truncatedInfo.addClass('hidden');
} }
this.checkAutoscroll();
if (!log.complete) { if (!log.complete) {
this.toggleScrollAnimation(true);
Build.timeout = setTimeout(() => { Build.timeout = setTimeout(() => {
this.invokeBuildTrace(); //eslint-disable-next-line
this.getBuildTrace()
.then(() => this.scrollToBottom());
}, 4000); }, 4000);
} else { } else {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
this.toggleScrollAnimation(false);
} }
if (log.status !== this.buildStatus) { if (log.status !== this.buildStatus) {
let pageUrl = this.pageUrl; gl.utils.visitUrl(this.pageUrl);
if (this.$autoScrollStatus.data('state') === 'enabled') {
pageUrl += DOWN_BUILD_TRACE;
}
gl.utils.visitUrl(pageUrl);
} }
}), })
error: () => { .fail(() => {
this.$buildRefreshAnimation.remove(); 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 () { Build.prototype.shouldHideSidebarForViewport = function () {
...@@ -257,18 +238,23 @@ window.Build = (function () { ...@@ -257,18 +238,23 @@ window.Build = (function () {
}; };
Build.prototype.toggleSidebar = function (shouldHide) { 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); .toggleClass('sidebar-collapsed', shouldHide);
this.$truncatedInfo.toggleClass('sidebar-expanded', shouldShow) this.$sidebar
.toggleClass('sidebar-collapsed', shouldHide); .toggleClass('right-sidebar-expanded', shouldShow)
this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide); .toggleClass('right-sidebar-collapsed', shouldHide);
}; };
Build.prototype.sidebarOnResize = function () { Build.prototype.sidebarOnResize = function () {
this.toggleSidebar(this.shouldHideSidebarForViewport()); this.toggleSidebar(this.shouldHideSidebarForViewport());
this.verifyTopPosition();
if (this.$scrollContainer.getNiceScroll(0)) {
this.toggleScroll();
}
}; };
Build.prototype.sidebarOnClick = function () { Build.prototype.sidebarOnClick = function () {
...@@ -301,24 +287,5 @@ window.Build = (function () { ...@@ -301,24 +287,5 @@ window.Build = (function () {
this.populateJobs(stage); 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; return Build;
})(); })();
...@@ -29,129 +29,140 @@ ...@@ -29,129 +29,140 @@
} }
} }
.build-page { @keyframes blinking-scroll-button {
pre.trace { 0% { opacity: 0.2; }
background: $builds-trace-bg; 25% { opacity: 0.5; }
color: $white-light; 50% { opacity: 0.7; }
font-family: $monospace_font; 100% { opacity: 1; }
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;
svg { .build-page {
position: relative; .sticky {
top: 1px; position: absolute;
margin-right: 5px; left: 0;
} right: 0;
} }
.truncated-info { .build-trace-container {
text-align: center; position: absolute;
border-bottom: 1px solid; top: 225px;
background-color: $black; left: 15px;
height: 45px; bottom: 10px;
padding: 15px; background: $black;
color: $gray-darkest;
font-family: $monospace_font;
font-size: 12px;
&.affix { &.sidebar-expanded {
top: 0; right: 305px;
} }
// with sidebar &.sidebar-collapsed {
&.affix.sidebar-expanded { right: 16px;
right: 312px;
left: 22px;
} }
// without sidebar code {
&.affix.sidebar-collapsed { background: $black;
right: 20px; color: $gray-darkest;
left: 20px;
} }
&.affix-top { .top-bar {
position: absolute;
top: 0; top: 0;
margin: 0 auto; height: 35px;
right: 5px; display: flex;
left: 5px; justify-content: flex-end;
} border-bottom: 1px outset $white-light;
.truncated-info-size { .truncated-info {
margin: 0 5px; margin: 0 auto;
} align-self: center;
.raw-link { .truncated-info-size {
color: inherit; margin: 0 5px;
margin-left: 5px; }
text-decoration: underline;
.raw-link {
color: inherit;
margin-left: 5px;
text-decoration: underline;
}
}
} }
}
}
.scroll-controls { .controllers {
height: 100%; display: flex;
align-self: center;
font-size: 15px;
.scroll-step { svg {
width: 31px; height: 15px;
margin: 0 0 0 auto; display: block;
} fill: $white-light;
}
.scroll-link, a,
.autoscroll-container { .btn-scroll {
right: 25px; margin: 0 8px;
z-index: 1; color: $white-light;
} }
.scroll-link { .btn-scroll.animate {
position: fixed; .first-triangle {
display: block; animation: blinking-scroll-button 1s ease infinite;
margin-bottom: 10px; animation-delay: .3s;
}
&.scroll-top .gitlab-icon-scroll-up-hover, .second-triangle {
&.scroll-top:hover .gitlab-icon-scroll-up, animation: blinking-scroll-button 1s ease infinite;
&.scroll-bottom .gitlab-icon-scroll-down-hover, animation-delay: .2s;
&.scroll-bottom:hover .gitlab-icon-scroll-down { }
display: none;
}
&.scroll-top:hover .gitlab-icon-scroll-up-hover, .third-triangle {
&.scroll-bottom:hover .gitlab-icon-scroll-down-hover { animation: blinking-scroll-button 1s ease infinite;
display: inline-block; }
}
&.scroll-top { &:disabled {
top: 10px; opacity: 1;
} }
}
&.scroll-bottom { .btn-scroll:disabled {
bottom: -2px; opacity: 0.35;
cursor: not-allowed;
}
} }
} }
.autoscroll-container { .bash {
position: absolute; 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, svg {
.autoscroll-container { position: relative;
right: ($gutter_width + ($gl-padding * 2)); 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 { .status-message {
...@@ -223,32 +234,6 @@ ...@@ -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 { .right-sidebar.build-sidebar {
padding: $gl-padding 0; padding: $gl-padding 0;
......
...@@ -68,15 +68,8 @@ ...@@ -68,15 +68,8 @@
- elsif @build.runner - elsif @build.runner
\##{@build.runner.id} \##{@build.runner.id}
.btn-group.btn-group-justified{ role: :group } .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? - if @build.active?
= link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post = 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 - if @build.trigger_request
.build-widget .build-widget
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- if @build.stuck? - if @build.stuck?
- unless @build.any_runners_online? - unless @build.any_runners_online?
.bs-callout.bs-callout-warning .bs-callout.bs-callout-warning.js-build-stuck
%p %p
- if no_runners_for_project?(@build.project) - if no_runners_for_project?(@build.project)
This job is stuck, because the project doesn't have any runners online assigned to it. This job is stuck, because the project doesn't have any runners online assigned to it.
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
Runners page Runners page
- if @build.starts_environment? - if @build.starts_environment?
.prepend-top-default .prepend-top-default.js-environment-container
.environment-information .environment-information
- if @build.outdated_deployment? - if @build.outdated_deployment?
= ci_icon_for_status('success_with_warnings') = ci_icon_for_status('success_with_warnings')
...@@ -47,39 +47,51 @@ ...@@ -47,39 +47,51 @@
- if environment.try(:last_deployment) - if environment.try(:last_deployment)
and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest 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? - if @build.erased?
.erased.alert.alert-warning .erased.alert.alert-warning
- if @build.erased_by_user? - 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)} Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else - else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)} Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls .prepend-top-default
.scroll-step .build-trace-container#build-trace
%a.scroll-link.scroll-top{ href: '#up-build-trace', id: 'scroll-top', title: 'Scroll to top' } .top-bar.sticky
= 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
.js-truncated-info.truncated-info.hidden< .js-truncated-info.truncated-info.hidden<
Showing last Showing last
%span.js-truncated-info-size.truncated-info-size>< %span.js-truncated-info-size.truncated-info-size><
KiB of log - KiB of log -
%a.js-raw-link.raw-link{ :href => raw_namespace_project_build_path(@project.namespace, @project, @build) }>< Complete Raw %a.js-raw-link.raw-link{ href: raw_namespace_project_build_path(@project.namespace, @project, @build) }>< Complete Raw
%code.bash.js-build-output .controllers
.build-loader-animation.js-build-refresh - 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" = render "sidebar"
......
<svg width="16" height="33" class="gitlab-icon-scroll-down" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg"> <svg width="12" height="16" viewBox="0 0 12 16" 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"/> <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>
<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"> <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>
<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="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 ...@@ -190,7 +190,7 @@ feature 'Builds', :feature do
end end
it do it do
expect(page).to have_link 'Raw' expect(page).to have_css('.js-raw-link')
end end
end end
...@@ -369,14 +369,14 @@ feature 'Builds', :feature do ...@@ -369,14 +369,14 @@ feature 'Builds', :feature do
end end
end end
describe 'GET /:project/builds/:id/raw' do describe 'GET /:project/builds/:id/raw', :js do
context 'access source' do context 'access source' do
context 'build from project' do context 'build from project' do
before 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! build.run!
visit namespace_project_build_path(project.namespace, project, build) visit namespace_project_build_path(project.namespace, project, build)
page.within('.js-build-sidebar') { click_link 'Raw' } find('.js-raw-link-controller').click()
end end
it 'sends the right headers' do it 'sends the right headers' do
...@@ -388,7 +388,7 @@ feature 'Builds', :feature do ...@@ -388,7 +388,7 @@ feature 'Builds', :feature do
context 'build from other project' do context 'build from other project' do
before 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! build2.run!
visit raw_namespace_project_build_path(project.namespace, project, build2) visit raw_namespace_project_build_path(project.namespace, project, build2)
end end
...@@ -403,7 +403,7 @@ feature 'Builds', :feature do ...@@ -403,7 +403,7 @@ feature 'Builds', :feature do
let(:existing_file) { Tempfile.new('existing-trace-file').path } let(:existing_file) { Tempfile.new('existing-trace-file').path }
before 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! build.run!
...@@ -413,13 +413,13 @@ feature 'Builds', :feature do ...@@ -413,13 +413,13 @@ feature 'Builds', :feature do
visit namespace_project_build_path(project.namespace, project, build) visit namespace_project_build_path(project.namespace, project, build)
end end
context 'when build has trace in file' do context 'when build has trace in file', :js do
let(:paths) do let(:paths) do
[existing_file] [existing_file]
end end
before do before do
page.within('.js-build-sidebar') { click_link 'Raw' } find('.js-raw-link-controller').click()
end end
it 'sends the right headers' do it 'sends the right headers' do
...@@ -433,7 +433,7 @@ feature 'Builds', :feature do ...@@ -433,7 +433,7 @@ feature 'Builds', :feature do
let(:paths) { [] } let(:paths) { [] }
it 'sends the right headers' do 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 end
end end
......
...@@ -14,7 +14,6 @@ describe('Build', () => { ...@@ -14,7 +14,6 @@ describe('Build', () => {
beforeEach(() => { beforeEach(() => {
loadFixtures('builds/build-with-artifacts.html.raw'); loadFixtures('builds/build-with-artifacts.html.raw');
spyOn($, 'ajax');
}); });
describe('class constructor', () => { describe('class constructor', () => {
...@@ -33,7 +32,6 @@ describe('Build', () => { ...@@ -33,7 +32,6 @@ describe('Build', () => {
it('copies build options', function () { it('copies build options', function () {
expect(this.build.pageUrl).toBe(BUILD_URL); expect(this.build.pageUrl).toBe(BUILD_URL);
expect(this.build.buildUrl).toBe(`${BUILD_URL}.json`);
expect(this.build.buildStatus).toBe('success'); expect(this.build.buildStatus).toBe('success');
expect(this.build.buildStage).toBe('test'); expect(this.build.buildStage).toBe('test');
expect(this.build.state).toBe(''); expect(this.build.state).toBe('');
...@@ -65,27 +63,14 @@ describe('Build', () => { ...@@ -65,27 +63,14 @@ describe('Build', () => {
}); });
describe('running build', () => { describe('running build', () => {
beforeEach(function () {
this.build = new Build();
});
it('updates the build trace on an interval', function () { 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'); spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001); deferred1.resolve({
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($, {
html: '<span>Update<span>', html: '<span>Update<span>',
status: 'running', status: 'running',
state: 'newstate', state: 'newstate',
...@@ -93,20 +78,9 @@ describe('Build', () => { ...@@ -93,20 +78,9 @@ describe('Build', () => {
complete: false, complete: false,
}); });
expect($('#build-trace .js-build-output').text()).toMatch(/Update/); deferred2.resolve();
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));
args.success.call($, { deferred3.resolve({
html: '<span>More</span>', html: '<span>More</span>',
status: 'running', status: 'running',
state: 'finalstate', state: 'finalstate',
...@@ -114,150 +88,222 @@ describe('Build', () => { ...@@ -114,150 +88,222 @@ describe('Build', () => {
complete: true, 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($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
expect(this.build.state).toBe('finalstate'); expect(this.build.state).toBe('finalstate');
}); });
it('replaces the entire build trace', () => { 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'); spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001); deferred1.resolve({
let args = $.ajax.calls.argsFor(0)[0]; html: '<span>Update<span>',
args.success.call($, {
html: '<span>Update</span>',
status: 'running', status: 'running',
append: false, append: false,
complete: false, complete: false,
}); });
expect($('#build-trace .js-build-output').text()).toMatch(/Update/); deferred2.resolve();
jasmine.clock().tick(4001); deferred3.resolve({
args = $.ajax.calls.argsFor(2)[0];
args.success.call($, {
html: '<span>Different</span>', html: '<span>Different</span>',
status: 'running', status: 'running',
append: false, 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()).not.toMatch(/Update/);
expect($('#build-trace .js-build-output').text()).toMatch(/Different/); expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
}); });
it('reloads the page when the build is done', () => { it('reloads the page when the build is done', () => {
spyOn(gl.utils, 'visitUrl'); spyOn(gl.utils, 'visitUrl');
const deferred = $.Deferred();
jasmine.clock().tick(4001); spyOn($, 'ajax').and.returnValue(deferred.promise());
const [{ success }] = $.ajax.calls.argsFor(0); deferred.resolve({
success.call($, {
html: '<span>Final</span>', html: '<span>Final</span>',
status: 'passed', status: 'passed',
append: true, append: true,
complete: true, complete: true,
}); });
this.build = new Build();
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL); expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
}); });
});
describe('truncated information', () => { describe('truncated information', () => {
describe('when size is less than total', () => { describe('when size is less than total', () => {
it('shows information about truncated log', () => { it('shows information about truncated log', () => {
jasmine.clock().tick(4001); spyOn(gl.utils, 'visitUrl');
const [{ success }] = $.ajax.calls.argsFor(0); const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
success.call($, {
html: '<span>Update</span>', deferred.resolve({
status: 'success', html: '<span>Update</span>',
append: false, status: 'success',
size: 50, append: false,
total: 100, size: 50,
}); total: 100,
expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
}); });
it('shows the size in KiB', () => { this.build = new Build();
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0); expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
const size = 50; });
success.call($, { it('shows the size in KiB', () => {
html: '<span>Update</span>', const size = 50;
status: 'success', spyOn(gl.utils, 'visitUrl');
append: false, const deferred = $.Deferred();
size,
total: 100, spyOn($, 'ajax').and.returnValue(deferred.promise());
}); deferred.resolve({
html: '<span>Update</span>',
expect( status: 'success',
document.querySelector('.js-truncated-info-size').textContent.trim(), append: false,
).toEqual(`${bytesToKiB(size)}`); size,
total: 100,
}); });
it('shows incremented size', () => { this.build = new Build();
jasmine.clock().tick(4001);
let args = $.ajax.calls.argsFor(0)[0]; expect(
args.success.call($, { document.querySelector('.js-truncated-info-size').textContent.trim(),
html: '<span>Update</span>', ).toEqual(`${bytesToKiB(size)}`);
status: 'success', });
append: false,
size: 50, it('shows incremented size', () => {
total: 100, const deferred1 = $.Deferred();
}); const deferred2 = $.Deferred();
const deferred3 = $.Deferred();
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(), spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
).toEqual(`${bytesToKiB(50)}`);
spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001);
args = $.ajax.calls.argsFor(2)[0]; deferred1.resolve({
args.success.call($, { html: '<span>Update</span>',
html: '<span>Update</span>', status: 'success',
status: 'success', append: false,
append: true, size: 50,
size: 10, total: 100,
total: 100,
});
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${bytesToKiB(60)}`);
}); });
it('renders the raw link', () => { deferred2.resolve();
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0); this.build = new Build();
success.call($, { expect(
html: '<span>Update</span>', document.querySelector('.js-truncated-info-size').textContent.trim(),
status: 'success', ).toEqual(`${bytesToKiB(50)}`);
append: false,
size: 50, jasmine.clock().tick(4001);
total: 100,
}); deferred3.resolve({
html: '<span>Update</span>',
expect( status: 'success',
document.querySelector('.js-raw-link').textContent.trim(), append: true,
).toContain('Complete Raw'); size: 10,
total: 100,
}); });
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${bytesToKiB(60)}`);
}); });
describe('when size is equal than total', () => { it('renders the raw link', () => {
it('does not show the trunctated information', () => { const deferred = $.Deferred();
jasmine.clock().tick(4001); spyOn(gl.utils, 'visitUrl');
const [{ success }] = $.ajax.calls.argsFor(0);
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
});
success.call($, { this.build = new Build();
html: '<span>Update</span>',
status: 'success',
append: false,
size: 100,
total: 100,
});
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