Skip to content
Snippets Groups Projects
Commit 452202e3 authored by Filipa Lacerda's avatar Filipa Lacerda Committed by Phil Hughes
Browse files

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

parent d25f6fcf
No related branches found
No related tags found
No related merge requests found
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