Skip to content
Snippets Groups Projects
Commit 194c40df authored by Phil Hughes's avatar Phil Hughes
Browse files

Merge branch '31397-job-detail-real-time' into 'master'

Improve Job detail view to make it refreshed in real-time instead of reloading

Closes #31397, #30901, #29948, and #24339

See merge request !11848
parents d25f6fcf 452202e3
No related branches found
No related tags found
2 merge requests!14773Maxraab master patch 51809,!11848Improve Job detail view to make it refreshed in real-time instead of reloading
Pipeline #
Showing
with 646 additions and 165 deletions
Loading
Loading
@@ -149,27 +149,34 @@ window.Build = (function () {
Build.prototype.verifyTopPosition = function () {
const $buildPage = $('.build-page');
 
const $flashError = $('.alert-wrapper');
const $header = $('.build-header', $buildPage);
const $runnersStuck = $('.js-build-stuck', $buildPage);
const $startsEnvironment = $('.js-environment-container', $buildPage);
const $erased = $('.js-build-erased', $buildPage);
const prependTopDefault = 20;
 
// header + navigation + margin
let topPostion = 168;
 
if ($header) {
if ($header.length) {
topPostion += $header.outerHeight();
}
 
if ($runnersStuck) {
if ($runnersStuck.length) {
topPostion += $runnersStuck.outerHeight();
}
 
if ($startsEnvironment) {
topPostion += $startsEnvironment.outerHeight();
if ($startsEnvironment.length) {
topPostion += $startsEnvironment.outerHeight() + prependTopDefault;
}
 
if ($erased) {
topPostion += $erased.outerHeight() + 10;
if ($erased.length) {
topPostion += $erased.outerHeight() + prependTopDefault;
}
if ($flashError.length) {
topPostion += $flashError.outerHeight();
}
 
this.$buildTrace.css({
Loading
Loading
@@ -245,6 +252,7 @@ window.Build = (function () {
 
Build.prototype.toggleSidebar = function (shouldHide) {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
const $toggleButton = $('.js-sidebar-build-toggle-header');
 
this.$buildTrace
.toggleClass('sidebar-expanded', shouldShow)
Loading
Loading
@@ -252,6 +260,16 @@ window.Build = (function () {
this.$sidebar
.toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide);
$('.js-build-page')
.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
if (this.$sidebar.hasClass('right-sidebar-expanded')) {
$toggleButton.addClass('hidden');
} else {
$toggleButton.removeClass('hidden');
}
};
 
Build.prototype.sidebarOnResize = function () {
Loading
Loading
@@ -266,6 +284,7 @@ window.Build = (function () {
 
Build.prototype.sidebarOnClick = function () {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
this.verifyTopPosition();
};
 
Build.prototype.updateArtifactRemoveDate = function () {
Loading
Loading
Loading
Loading
@@ -2,7 +2,6 @@
/* global UsernameValidator */
/* global ActiveTabMemoizer */
/* global ShortcutsNavigation */
/* global Build */
/* global IssuableIndex */
/* global ShortcutsIssuable */
/* global ZenMode */
Loading
Loading
@@ -119,9 +118,6 @@ import initSettingsPanels from './settings_panels';
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break;
case 'projects:jobs:show':
new Build();
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) {
Loading
Loading
<script>
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
name: 'jobHeaderSection',
props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
},
components: {
ciHeader,
loadingIcon,
},
data() {
return {
actions: this.getActions(),
};
},
computed: {
status() {
return this.job && this.job.status;
},
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length;
},
},
methods: {
getActions() {
const actions = [];
if (this.job.new_issue_path) {
actions.push({
label: 'New issue',
path: this.job.new_issue_path,
cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
type: 'ujs-link',
});
}
if (this.job.retry_path) {
actions.push({
label: 'Retry',
path: this.job.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary visible-md-block visible-lg-block',
type: 'ujs-link',
});
}
return actions;
},
},
watch: {
job() {
this.actions = this.getActions();
},
},
};
</script>
<template>
<div class="js-build-header build-header top-area">
<ci-header
v-if="shouldRenderContent"
:status="status"
item-name="Job"
:item-id="job.id"
:time="job.created_at"
:user="job.user"
:actions="actions"
:hasSidebarButton="true"
/>
<loading-icon
v-if="isLoading"
size="2"
/>
</div>
</template>
<script>
export default {
name: 'SidebarDetailRow',
props: {
title: {
type: String,
required: false,
default: '',
},
value: {
type: String,
required: true,
},
},
computed: {
hasTitle() {
return this.title.length > 0;
},
},
};
</script>
<template>
<p class="build-detail-row">
<span
v-if="hasTitle"
class="build-light-text">
{{title}}:
</span>
{{value}}
</p>
</template>
<script>
import detailRow from './sidebar_detail_row.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
export default {
name: 'SidebarDetailsBlock',
props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
},
mixins: [
timeagoMixin,
],
components: {
detailRow,
loadingIcon,
},
computed: {
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length > 0;
},
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `#${this.job.runner.id}`;
},
},
};
</script>
<template>
<div>
<template v-if="shouldRenderContent">
<div
class="block retry-link"
v-if="job.retry_path || job.new_issue_path">
<a
v-if="job.new_issue_path"
class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path">
New issue
</a>
<a
v-if="job.retry_path"
class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path"
data-method="post"
rel="nofollow">
Retry
</a>
</div>
<div class="block">
<p
class="build-detail-row js-job-mr"
v-if="job.merge_request">
<span
class="build-light-text">
Merge Request:
</span>
<a :href="job.merge_request.path">
!{{job.merge_request.iid}}
</a>
</p>
<detail-row
class="js-job-duration"
v-if="job.duration"
title="Duration"
:value="duration"
/>
<detail-row
class="js-job-finished"
v-if="job.finished_at"
title="Finished"
:value="timeFormated(job.finished_at)"
/>
<detail-row
class="js-job-erased"
v-if="job.erased_at"
title="Erased"
:value="timeFormated(job.erased_at)"
/>
<detail-row
class="js-job-queued"
v-if="job.queued"
title="Queued"
:value="queued"
/>
<detail-row
class="js-job-runner"
v-if="job.runner"
title="Runner"
:value="runnerId"
/>
<detail-row
class="js-job-coverage"
v-if="job.coverage"
title="Coverage"
:value="coverage"
/>
<p
class="build-detail-row js-job-tags"
v-if="job.tags.length">
<span
class="build-light-text">
Tags:
</span>
<span
v-for="tag in job.tags"
key="tag"
class="label label-primary">
{{tag}}
</span>
</p>
<div
v-if="job.cancel_path"
class="btn-group prepend-top-5"
role="group">
<a
class="js-cancel-job btn btn-sm btn-default"
:href="job.cancel_path"
data-method="post"
rel="nofollow">
Cancel
</a>
</div>
</div>
</template>
<loading-icon
class="prepend-top-10"
v-if="isLoading"
size="2"
/>
</div>
</template>
/* global Flash */
import Vue from 'vue';
import JobMediator from './job_details_mediator';
import jobHeader from './components/header.vue';
import detailsBlock from './components/sidebar_details_block.vue';
document.addEventListener('DOMContentLoaded', () => {
const dataset = document.getElementById('js-job-details-vue').dataset;
const mediator = new JobMediator({ endpoint: dataset.endpoint });
mediator.fetchJob();
// Header
// eslint-disable-next-line no-new
new Vue({
el: '#js-build-header-vue',
data() {
return {
mediator,
};
},
components: {
jobHeader,
},
mounted() {
this.mediator.initBuildClass();
},
updated() {
// Wait for flash message to be appended
Vue.nextTick(() => {
if (this.mediator.build) {
this.mediator.build.verifyTopPosition();
}
});
},
render(createElement) {
return createElement('job-header', {
props: {
isLoading: this.mediator.state.isLoading,
job: this.mediator.store.state.job,
},
});
},
});
// Sidebar information block
// eslint-disable-next-line
new Vue({
el: '#js-details-block-vue',
data() {
return {
mediator,
};
},
components: {
detailsBlock,
},
render(createElement) {
return createElement('details-block', {
props: {
isLoading: this.mediator.state.isLoading,
job: this.mediator.store.state.job,
},
});
},
});
});
/* global Flash */
/* global Build */
import Visibility from 'visibilityjs';
import Poll from '../lib/utils/poll';
import JobStore from './stores/job_store';
import JobService from './services/job_service';
import '../build';
export default class JobMediator {
constructor(options = {}) {
this.options = options;
this.store = new JobStore();
this.service = new JobService(options.endpoint);
this.state = {
isLoading: false,
};
}
initBuildClass() {
this.build = new Build();
}
fetchJob() {
this.poll = new Poll({
resource: this.service,
method: 'getJob',
successCallback: this.successCallback.bind(this),
errorCallback: this.errorCallback.bind(this),
});
if (!Visibility.hidden()) {
this.state.isLoading = true;
this.poll.makeRequest();
} else {
this.getJob();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}
getJob() {
return this.service.getJob()
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
successCallback(response) {
const data = response.json();
this.state.isLoading = false;
this.store.storeJob(data);
}
errorCallback() {
this.state.isLoading = false;
return new Flash('An error occurred while fetching the job.');
}
}
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class JobService {
constructor(endpoint) {
this.job = Vue.resource(endpoint);
}
getJob() {
return this.job.get();
}
}
export default class JobStore {
constructor() {
this.state = {
job: {},
};
}
storeJob(job = {}) {
this.state.job = job;
}
}
Loading
Loading
@@ -146,3 +146,24 @@ window.dateFormat = dateFormat;
};
})(window);
}).call(window);
/**
* Port of ruby helper time_interval_in_words.
*
* @param {Number} seconds
* @return {String}
*/
// eslint-disable-next-line import/prefer-default-export
export function timeIntervalInWords(intervalInSeconds) {
const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60);
const seconds = secondsInteger - (minutes * 60);
let text = '';
if (minutes >= 1) {
text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`;
} else {
text = `${seconds} ${gl.text.pluralize('second', seconds)}`;
}
return text;
}
Loading
Loading
@@ -91,7 +91,7 @@ export default {
@actionClicked="postAction"
/>
<loading-icon
v-else
v-if="isLoading"
size="2"/>
</div>
</template>
Loading
Loading
@@ -40,6 +40,11 @@ export default {
required: false,
default: () => [],
},
hasSidebarButton: {
type: Boolean,
required: false,
default: false,
},
},
 
mixins: [
Loading
Loading
@@ -66,8 +71,9 @@ export default {
},
};
</script>
<template>
<header class="page-content-header">
<header class="page-content-header ci-header-container">
<section class="header-main-content">
 
<ci-icon-badge :status="status" />
Loading
Loading
@@ -102,7 +108,7 @@ export default {
</section>
 
<section
class="header-action-button nav-controls"
class="header-action-buttons"
v-if="actions.length">
<template
v-for="action in actions">
Loading
Loading
@@ -113,6 +119,15 @@ export default {
{{action.label}}
</a>
 
<a
v-if="action.type === 'ujs-link'"
:href="action.path"
data-method="post"
rel="nofollow"
:class="action.cssClass">
{{action.label}}
</a>
<button
v-else="action.type === 'button'"
@click="onClickAction(action)"
Loading
Loading
@@ -120,7 +135,6 @@ export default {
:class="action.cssClass"
type="button">
{{action.label}}
<i
v-show="action.isLoading"
class="fa fa-spin fa-spinner"
Loading
Loading
@@ -128,6 +142,18 @@ export default {
</i>
</button>
</template>
<button
v-if="hasSidebarButton"
type="button"
class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
aria-label="Toggle Sidebar"
id="toggleSidebar">
<i
class="fa fa-angle-double-left"
aria-hidden="true"
aria-labelledby="toggleSidebar">
</i>
</button>
</section>
</header>
</template>
Loading
Loading
@@ -153,15 +153,16 @@
}
 
.environment-information {
background-color: $gray-light;
border: 1px solid $border-color;
padding: 12px $gl-padding;
padding: 8px $gl-padding 12px;
border-radius: $border-radius-default;
 
svg {
position: relative;
top: 1px;
top: 5px;
margin-right: 5px;
width: 22px;
height: 22px;
}
}
 
Loading
Loading
@@ -175,54 +176,31 @@
}
}
 
.status-message {
display: inline-block;
color: $white-light;
.status-icon {
display: inline-block;
width: 16px;
height: 33px;
.build-header {
.ci-header-container,
.header-action-buttons {
display: flex;
}
 
.status-text {
float: left;
opacity: 0;
margin-right: 10px;
font-weight: normal;
line-height: 1.8;
transition: opacity 1s ease-out;
&.animate {
animation: fade-out-status 2s ease;
}
.ci-header-container {
min-height: 54px;
}
 
&:hover .status-text {
opacity: 1;
.page-content-header {
padding: 10px 0 9px;
}
}
.build-header {
position: relative;
padding: 0;
display: flex;
min-height: 58px;
align-items: center;
@media (max-width: $screen-sm-max) {
padding-right: 40px;
margin-top: 6px;
 
.btn-inverted {
display: none;
.header-action-buttons {
@media (max-width: $screen-xs-max) {
.sidebar-toggle-btn {
margin-top: 0;
margin-left: 10px;
max-height: 34px;
}
}
}
 
.header-content {
flex: 1;
line-height: 1.8;
a {
color: $gl-text-color;
 
Loading
Loading
@@ -245,7 +223,7 @@
}
 
.right-sidebar.build-sidebar {
padding: $gl-padding 0;
padding: 0;
 
&.right-sidebar-collapsed {
display: none;
Loading
Loading
@@ -258,6 +236,10 @@
.block {
width: 100%;
 
&:last-child {
border-bottom: 1px solid $border-gray-normal;
}
&.coverage {
padding: 0 16px 11px;
}
Loading
Loading
@@ -267,34 +249,39 @@
}
}
 
.js-build-variable {
.trigger-build-variable {
color: $code-color;
}
 
.js-build-value {
.trigger-build-value {
padding: 2px 4px;
color: $black;
background-color: $white-light;
}
 
.build-sidebar-header {
padding: 0 $gl-padding $gl-padding;
.gutter-toggle {
margin-top: 0;
}
.label {
margin-left: 2px;
}
 
.retry-link {
color: $gl-link-color;
display: none;
 
&:hover {
text-decoration: underline;
.btn-inverted-secondary {
color: $blue-500;
&:hover {
color: $white-light;
}
}
 
@media (max-width: $screen-sm-max) {
display: block;
.btn {
i {
margin-left: 5px;
}
}
}
}
 
Loading
Loading
@@ -318,6 +305,12 @@
left: $gl-padding;
width: auto;
}
svg {
position: relative;
top: 2px;
margin-right: 3px;
}
}
 
.builds-container {
Loading
Loading
@@ -379,6 +372,10 @@
}
}
}
.link-commit {
color: $blue-600;
}
}
 
.build-sidebar {
Loading
Loading
Loading
Loading
@@ -986,10 +986,17 @@
}
}
 
.pipeline-header-container {
.ci-header-container {
min-height: 55px;
 
.text-center {
padding-top: 12px;
}
.header-action-buttons {
.btn,
a {
margin-left: 10px;
}
}
}
Loading
Loading
@@ -132,6 +132,11 @@ class CommitStatus < ActiveRecord::Base
false
end
 
# To be overriden when inherrited from
def cancelable?
false
end
def stuck?
false
end
Loading
Loading
Loading
Loading
@@ -34,10 +34,8 @@ class BuildDetailsEntity < BuildEntity
private
 
def build_failed_issue_options
{
title: "Build Failed ##{build.id}",
description: namespace_project_job_url(project.namespace, project, build)
}
{ title: "Build Failed ##{build.id}",
description: namespace_project_job_path(project.namespace, project, build) }
end
 
def current_user
Loading
Loading
Loading
Loading
@@ -8,10 +8,14 @@ class BuildEntity < Grape::Entity
path_to(:namespace_project_job, build)
end
 
expose :retry_path, if: -> (*) { build&.retryable? } do |build|
expose :retry_path, if: -> (*) { retryable? } do |build|
path_to(:retry_namespace_project_job, build)
end
 
expose :cancel_path, if: -> (*) { cancelable? } do |build|
path_to(:cancel_namespace_project_job, build)
end
expose :play_path, if: -> (*) { playable? } do |build|
path_to(:play_namespace_project_job, build)
end
Loading
Loading
@@ -25,6 +29,14 @@ class BuildEntity < Grape::Entity
 
alias_method :build, :object
 
def cancelable?
build.cancelable? && can?(request.current_user, :update_build, build)
end
def retryable?
build.retryable? && can?(request.current_user, :update_build, build)
end
def playable?
build.playable? && can?(request.current_user, :update_build, build)
end
Loading
Loading
- builds = @build.pipeline.builds.to_a
 
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
Job
%strong ##{@build.id}
%a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
= icon('angle-double-right')
- if @build.coverage
.block.coverage
.title
Test coverage
%p.build-detail-row
#{@build.coverage}%
.blocks-container
.block
%strong
= @build.name
%a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' }
= icon('angle-double-right')
#js-details-block-vue
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block{ class: ("block-first" if !@build.coverage) }
.title
Loading
Loading
@@ -40,37 +36,6 @@
= link_to browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
Browse
 
.block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
.title
Job details
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry job", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- if @build.merge_request
%p.build-detail-row
%span.build-light-text Merge Request:
= link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'bold'
- if @build.duration
%p.build-detail-row
%span.build-light-text Duration:
= time_interval_in_words(@build.duration)
- if @build.finished_at
%p.build-detail-row
%span.build-light-text Finished:
#{time_ago_with_tooltip(@build.finished_at)}
- if @build.erased_at
%p.build-detail-row
%span.build-light-text Erased:
#{time_ago_with_tooltip(@build.erased_at)}
%p.build-detail-row
%span.build-light-text Runner:
- if @build.runner && current_user && current_user.admin
= link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
- elsif @build.runner
\##{@build.runner.id}
.btn-group.btn-group-justified{ role: :group }
- if @build.active?
= link_to "Cancel", cancel_namespace_project_job_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
- if @build.trigger_request
.build-widget
%h4.title
Loading
Loading
@@ -87,26 +52,29 @@
 
- @build.trigger_request.variables.each do |key, value|
.hide.js-build
.js-build-variable= key
.js-build-value= value
.js-build-variable.trigger-build-variable= key
.js-build-value.trigger-build-value= value
 
.block
.title
Commit title
%p
Commit
= link_to @build.pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, @build.pipeline.sha), class: 'commit-sha link-commit'
= clipboard_button(text: @build.pipeline.short_sha, title: "Copy commit SHA to clipboard")
- if @build.merge_request
in
= link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'link-commit'
%p.build-light-text.append-bottom-0
#{@build.pipeline.git_commit_title}
 
- if @build.tags.any?
.block
.title
Tags
- @build.tag_list.each do |tag|
%span.label.label-primary
= tag
- if @build.pipeline.stages_count > 1
.dropdown.build-dropdown
.title Stage
.title
%span{ class: "ci-status-icon-#{@build.pipeline.status}" }
= ci_icon_for_status(@build.pipeline.status)
= link_to "##{@build.pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @build.pipeline), class: 'link-commit'
from
= link_to "#{@build.pipeline.ref}", namespace_project_branch_path(@project.namespace, @project, @build.pipeline.ref), class: 'link-commit'
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.stage-selection More
= icon('chevron-down')
Loading
Loading
Loading
Loading
@@ -3,9 +3,8 @@
= render "projects/pipelines/head"
 
%div{ class: container_class }
.build-page
= render "header"
.build-page.js-build-page
#js-build-header-vue
- if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning.js-build-stuck
Loading
Loading
@@ -47,47 +46,52 @@
- if environment.try(:last_deployment)
and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
 
.prepend-top-default.js-build-erased
- if @build.erased?
- if @build.erased?
.prepend-top-default.js-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)}
 
.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_job_path(@project.namespace, @project, @build) }>< Complete Raw
.controllers
- if @build.has_trace?
= link_to raw_namespace_project_job_path(@project.namespace, @project, @build),
title: 'Show complete raw',
data: { placement: 'top', container: 'body' },
class: 'js-raw-link-controller has-tooltip controllers-buttons' do
= icon('file-text-o')
.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_job_path(@project.namespace, @project, @build) }>< Complete Raw
.controllers
- if @build.has_trace?
= link_to raw_namespace_project_job_path(@project.namespace, @project, @build),
title: 'Show complete raw',
data: { placement: 'top', container: 'body' },
class: 'js-raw-link-controller has-tooltip controllers-buttons' do
= icon('file-text-o')
 
- if can?(current_user, :update_build, @project) && @build.erasable?
= link_to erase_namespace_project_job_path(@project.namespace, @project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
title: 'Erase job log',
class: 'has-tooltip js-erase-link controllers-buttons' do
= icon('trash')
.has-tooltip.controllers-buttons{ title: 'Scroll to top', data: { placement: 'top', container: 'body'} }
%button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_up')
.has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} }
%button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_down')
.bash.sticky.js-scroll-container
%code.js-build-output
.build-loader-animation.js-build-refresh
- if can?(current_user, :update_build, @project) && @build.erasable?
= link_to erase_namespace_project_job_path(@project.namespace, @project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
title: 'Erase job log',
class: 'has-tooltip js-erase-link controllers-buttons' do
= icon('trash')
.has-tooltip.controllers-buttons{ title: 'Scroll to top', data: { placement: 'top', container: 'body'} }
%button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_up')
.has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} }
%button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_down')
.bash.sticky.js-scroll-container
%code.js-build-output
.build-loader-animation.js-build-refresh
 
= render "sidebar"
 
.js-build-options{ data: javascript_build_options }
#js-job-details-vue{ data: { endpoint: namespace_project_job_path(@project.namespace, @project, @build, format: :json) } }
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('job_details')
---
title: Adds realtime feature to job show view header and sidebar info. Updates UX.
merge_request:
author:
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment