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
@@ -149,27 +149,34 @@ window.Build = (function () {
Loading
@@ -149,27 +149,34 @@ window.Build = (function () {
Build.prototype.verifyTopPosition = function () { Build.prototype.verifyTopPosition = function () {
const $buildPage = $('.build-page'); const $buildPage = $('.build-page');
   
const $flashError = $('.alert-wrapper');
const $header = $('.build-header', $buildPage); const $header = $('.build-header', $buildPage);
const $runnersStuck = $('.js-build-stuck', $buildPage); const $runnersStuck = $('.js-build-stuck', $buildPage);
const $startsEnvironment = $('.js-environment-container', $buildPage); const $startsEnvironment = $('.js-environment-container', $buildPage);
const $erased = $('.js-build-erased', $buildPage); const $erased = $('.js-build-erased', $buildPage);
const prependTopDefault = 20;
   
// header + navigation + margin
let topPostion = 168; let topPostion = 168;
   
if ($header) { if ($header.length) {
topPostion += $header.outerHeight(); topPostion += $header.outerHeight();
} }
   
if ($runnersStuck) { if ($runnersStuck.length) {
topPostion += $runnersStuck.outerHeight(); topPostion += $runnersStuck.outerHeight();
} }
   
if ($startsEnvironment) { if ($startsEnvironment.length) {
topPostion += $startsEnvironment.outerHeight(); topPostion += $startsEnvironment.outerHeight() + prependTopDefault;
} }
   
if ($erased) { if ($erased.length) {
topPostion += $erased.outerHeight() + 10; topPostion += $erased.outerHeight() + prependTopDefault;
}
if ($flashError.length) {
topPostion += $flashError.outerHeight();
} }
   
this.$buildTrace.css({ this.$buildTrace.css({
Loading
@@ -245,6 +252,7 @@ window.Build = (function () {
Loading
@@ -245,6 +252,7 @@ window.Build = (function () {
   
Build.prototype.toggleSidebar = function (shouldHide) { Build.prototype.toggleSidebar = function (shouldHide) {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
const $toggleButton = $('.js-sidebar-build-toggle-header');
   
this.$buildTrace this.$buildTrace
.toggleClass('sidebar-expanded', shouldShow) .toggleClass('sidebar-expanded', shouldShow)
Loading
@@ -252,6 +260,16 @@ window.Build = (function () {
Loading
@@ -252,6 +260,16 @@ window.Build = (function () {
this.$sidebar this.$sidebar
.toggleClass('right-sidebar-expanded', shouldShow) .toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide); .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 () { Build.prototype.sidebarOnResize = function () {
Loading
@@ -266,6 +284,7 @@ window.Build = (function () {
Loading
@@ -266,6 +284,7 @@ window.Build = (function () {
   
Build.prototype.sidebarOnClick = function () { Build.prototype.sidebarOnClick = function () {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
this.verifyTopPosition();
}; };
   
Build.prototype.updateArtifactRemoveDate = function () { Build.prototype.updateArtifactRemoveDate = function () {
Loading
Loading
Loading
@@ -2,7 +2,6 @@
Loading
@@ -2,7 +2,6 @@
/* global UsernameValidator */ /* global UsernameValidator */
/* global ActiveTabMemoizer */ /* global ActiveTabMemoizer */
/* global ShortcutsNavigation */ /* global ShortcutsNavigation */
/* global Build */
/* global IssuableIndex */ /* global IssuableIndex */
/* global ShortcutsIssuable */ /* global ShortcutsIssuable */
/* global ZenMode */ /* global ZenMode */
Loading
@@ -119,9 +118,6 @@ import initSettingsPanels from './settings_panels';
Loading
@@ -119,9 +118,6 @@ import initSettingsPanels from './settings_panels';
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new UsersSelect(); new UsersSelect();
break; break;
case 'projects:jobs:show':
new Build();
break;
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) { 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
@@ -146,3 +146,24 @@ window.dateFormat = dateFormat;
Loading
@@ -146,3 +146,24 @@ window.dateFormat = dateFormat;
}; };
})(window); })(window);
}).call(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
@@ -91,7 +91,7 @@ export default {
Loading
@@ -91,7 +91,7 @@ export default {
@actionClicked="postAction" @actionClicked="postAction"
/> />
<loading-icon <loading-icon
v-else v-if="isLoading"
size="2"/> size="2"/>
</div> </div>
</template> </template>
Loading
@@ -40,6 +40,11 @@ export default {
Loading
@@ -40,6 +40,11 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
hasSidebarButton: {
type: Boolean,
required: false,
default: false,
},
}, },
   
mixins: [ mixins: [
Loading
@@ -66,8 +71,9 @@ export default {
Loading
@@ -66,8 +71,9 @@ export default {
}, },
}; };
</script> </script>
<template> <template>
<header class="page-content-header"> <header class="page-content-header ci-header-container">
<section class="header-main-content"> <section class="header-main-content">
   
<ci-icon-badge :status="status" /> <ci-icon-badge :status="status" />
Loading
@@ -102,7 +108,7 @@ export default {
Loading
@@ -102,7 +108,7 @@ export default {
</section> </section>
   
<section <section
class="header-action-button nav-controls" class="header-action-buttons"
v-if="actions.length"> v-if="actions.length">
<template <template
v-for="action in actions"> v-for="action in actions">
Loading
@@ -113,6 +119,15 @@ export default {
Loading
@@ -113,6 +119,15 @@ export default {
{{action.label}} {{action.label}}
</a> </a>
   
<a
v-if="action.type === 'ujs-link'"
:href="action.path"
data-method="post"
rel="nofollow"
:class="action.cssClass">
{{action.label}}
</a>
<button <button
v-else="action.type === 'button'" v-else="action.type === 'button'"
@click="onClickAction(action)" @click="onClickAction(action)"
Loading
@@ -120,7 +135,6 @@ export default {
Loading
@@ -120,7 +135,6 @@ export default {
:class="action.cssClass" :class="action.cssClass"
type="button"> type="button">
{{action.label}} {{action.label}}
<i <i
v-show="action.isLoading" v-show="action.isLoading"
class="fa fa-spin fa-spinner" class="fa fa-spin fa-spinner"
Loading
@@ -128,6 +142,18 @@ export default {
Loading
@@ -128,6 +142,18 @@ export default {
</i> </i>
</button> </button>
</template> </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> </section>
</header> </header>
</template> </template>
Loading
@@ -153,15 +153,16 @@
Loading
@@ -153,15 +153,16 @@
} }
   
.environment-information { .environment-information {
background-color: $gray-light;
border: 1px solid $border-color; border: 1px solid $border-color;
padding: 12px $gl-padding; padding: 8px $gl-padding 12px;
border-radius: $border-radius-default; border-radius: $border-radius-default;
   
svg { svg {
position: relative; position: relative;
top: 1px; top: 5px;
margin-right: 5px; margin-right: 5px;
width: 22px;
height: 22px;
} }
} }
   
Loading
@@ -175,54 +176,31 @@
Loading
@@ -175,54 +176,31 @@
} }
} }
   
.status-message { .build-header {
display: inline-block; .ci-header-container,
color: $white-light; .header-action-buttons {
display: flex;
.status-icon {
display: inline-block;
width: 16px;
height: 33px;
} }
   
.status-text { .ci-header-container {
float: left; min-height: 54px;
opacity: 0;
margin-right: 10px;
font-weight: normal;
line-height: 1.8;
transition: opacity 1s ease-out;
&.animate {
animation: fade-out-status 2s ease;
}
} }
   
&:hover .status-text { .page-content-header {
opacity: 1; 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 { .header-action-buttons {
display: none; @media (max-width: $screen-xs-max) {
.sidebar-toggle-btn {
margin-top: 0;
margin-left: 10px;
max-height: 34px;
}
} }
} }
   
.header-content { .header-content {
flex: 1;
line-height: 1.8;
a { a {
color: $gl-text-color; color: $gl-text-color;
   
Loading
@@ -245,7 +223,7 @@
Loading
@@ -245,7 +223,7 @@
} }
   
.right-sidebar.build-sidebar { .right-sidebar.build-sidebar {
padding: $gl-padding 0; padding: 0;
   
&.right-sidebar-collapsed { &.right-sidebar-collapsed {
display: none; display: none;
Loading
@@ -258,6 +236,10 @@
Loading
@@ -258,6 +236,10 @@
.block { .block {
width: 100%; width: 100%;
   
&:last-child {
border-bottom: 1px solid $border-gray-normal;
}
&.coverage { &.coverage {
padding: 0 16px 11px; padding: 0 16px 11px;
} }
Loading
@@ -267,34 +249,39 @@
Loading
@@ -267,34 +249,39 @@
} }
} }
   
.js-build-variable { .trigger-build-variable {
color: $code-color; color: $code-color;
} }
   
.js-build-value { .trigger-build-value {
padding: 2px 4px; padding: 2px 4px;
color: $black; color: $black;
background-color: $white-light; background-color: $white-light;
} }
   
.build-sidebar-header { .label {
padding: 0 $gl-padding $gl-padding; margin-left: 2px;
.gutter-toggle {
margin-top: 0;
}
} }
   
.retry-link { .retry-link {
color: $gl-link-color;
display: none; display: none;
   
&:hover { .btn-inverted-secondary {
text-decoration: underline; color: $blue-500;
&:hover {
color: $white-light;
}
} }
   
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
display: block; display: block;
.btn {
i {
margin-left: 5px;
}
}
} }
} }
   
Loading
@@ -318,6 +305,12 @@
Loading
@@ -318,6 +305,12 @@
left: $gl-padding; left: $gl-padding;
width: auto; width: auto;
} }
svg {
position: relative;
top: 2px;
margin-right: 3px;
}
} }
   
.builds-container { .builds-container {
Loading
@@ -379,6 +372,10 @@
Loading
@@ -379,6 +372,10 @@
} }
} }
} }
.link-commit {
color: $blue-600;
}
} }
   
.build-sidebar { .build-sidebar {
Loading
Loading
Loading
@@ -986,10 +986,17 @@
Loading
@@ -986,10 +986,17 @@
} }
} }
   
.pipeline-header-container { .ci-header-container {
min-height: 55px; min-height: 55px;
   
.text-center { .text-center {
padding-top: 12px; padding-top: 12px;
} }
.header-action-buttons {
.btn,
a {
margin-left: 10px;
}
}
} }
Loading
@@ -132,6 +132,11 @@ class CommitStatus < ActiveRecord::Base
Loading
@@ -132,6 +132,11 @@ class CommitStatus < ActiveRecord::Base
false false
end end
   
# To be overriden when inherrited from
def cancelable?
false
end
def stuck? def stuck?
false false
end end
Loading
Loading
Loading
@@ -34,10 +34,8 @@ class BuildDetailsEntity < BuildEntity
Loading
@@ -34,10 +34,8 @@ class BuildDetailsEntity < BuildEntity
private private
   
def build_failed_issue_options def build_failed_issue_options
{ { title: "Build Failed ##{build.id}",
title: "Build Failed ##{build.id}", description: namespace_project_job_path(project.namespace, project, build) }
description: namespace_project_job_url(project.namespace, project, build)
}
end end
   
def current_user def current_user
Loading
Loading
Loading
@@ -8,10 +8,14 @@ class BuildEntity < Grape::Entity
Loading
@@ -8,10 +8,14 @@ class BuildEntity < Grape::Entity
path_to(:namespace_project_job, build) path_to(:namespace_project_job, build)
end end
   
expose :retry_path, if: -> (*) { build&.retryable? } do |build| expose :retry_path, if: -> (*) { retryable? } do |build|
path_to(:retry_namespace_project_job, build) path_to(:retry_namespace_project_job, build)
end end
   
expose :cancel_path, if: -> (*) { cancelable? } do |build|
path_to(:cancel_namespace_project_job, build)
end
expose :play_path, if: -> (*) { playable? } do |build| expose :play_path, if: -> (*) { playable? } do |build|
path_to(:play_namespace_project_job, build) path_to(:play_namespace_project_job, build)
end end
Loading
@@ -25,6 +29,14 @@ class BuildEntity < Grape::Entity
Loading
@@ -25,6 +29,14 @@ class BuildEntity < Grape::Entity
   
alias_method :build, :object 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? def playable?
build.playable? && can?(request.current_user, :update_build, build) build.playable? && can?(request.current_user, :update_build, build)
end end
Loading
Loading
- builds = @build.pipeline.builds.to_a - 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" } } %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 .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?) - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block{ class: ("block-first" if !@build.coverage) } .block{ class: ("block-first" if !@build.coverage) }
.title .title
Loading
@@ -40,37 +36,6 @@
Loading
@@ -40,37 +36,6 @@
= link_to browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do = link_to browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
Browse 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 - if @build.trigger_request
.build-widget .build-widget
%h4.title %h4.title
Loading
@@ -87,26 +52,29 @@
Loading
@@ -87,26 +52,29 @@
   
- @build.trigger_request.variables.each do |key, value| - @build.trigger_request.variables.each do |key, value|
.hide.js-build .hide.js-build
.js-build-variable= key .js-build-variable.trigger-build-variable= key
.js-build-value= value .js-build-value.trigger-build-value= value
   
.block .block
.title %p
Commit title 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 %p.build-light-text.append-bottom-0
#{@build.pipeline.git_commit_title} #{@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 - if @build.pipeline.stages_count > 1
.dropdown.build-dropdown .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' } %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.stage-selection More %span.stage-selection More
= icon('chevron-down') = icon('chevron-down')
Loading
Loading
Loading
@@ -3,9 +3,8 @@
Loading
@@ -3,9 +3,8 @@
= render "projects/pipelines/head" = render "projects/pipelines/head"
   
%div{ class: container_class } %div{ class: container_class }
.build-page .build-page.js-build-page
= render "header" #js-build-header-vue
- if @build.stuck? - if @build.stuck?
- unless @build.any_runners_online? - unless @build.any_runners_online?
.bs-callout.bs-callout-warning.js-build-stuck .bs-callout.bs-callout-warning.js-build-stuck
Loading
@@ -47,47 +46,52 @@
Loading
@@ -47,47 +46,52 @@
- 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.js-build-erased - if @build.erased?
- if @build.erased? .prepend-top-default.js-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)}
   
.prepend-top-default .build-trace-container#build-trace
.build-trace-container#build-trace .top-bar.sticky
.top-bar.sticky .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_job_path(@project.namespace, @project, @build) }>< Complete Raw
%a.js-raw-link.raw-link{ href: raw_namespace_project_job_path(@project.namespace, @project, @build) }>< Complete Raw .controllers
.controllers - if @build.has_trace?
- if @build.has_trace? = link_to raw_namespace_project_job_path(@project.namespace, @project, @build),
= link_to raw_namespace_project_job_path(@project.namespace, @project, @build), title: 'Show complete raw',
title: 'Show complete raw', data: { placement: 'top', container: 'body' },
data: { placement: 'top', container: 'body' }, class: 'js-raw-link-controller has-tooltip controllers-buttons' do
class: 'js-raw-link-controller has-tooltip controllers-buttons' do = icon('file-text-o')
= icon('file-text-o')
   
- if can?(current_user, :update_build, @project) && @build.erasable? - if can?(current_user, :update_build, @project) && @build.erasable?
= link_to erase_namespace_project_job_path(@project.namespace, @project, @build), = link_to erase_namespace_project_job_path(@project.namespace, @project, @build),
method: :post, method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' }, data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
title: 'Erase job log', title: 'Erase job log',
class: 'has-tooltip js-erase-link controllers-buttons' do class: 'has-tooltip js-erase-link controllers-buttons' do
= icon('trash') = icon('trash')
.has-tooltip.controllers-buttons{ title: 'Scroll to top', data: { placement: 'top', container: 'body'} } .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 } %button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_up') = custom_icon('scroll_up')
.has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} } .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 } %button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_down') = custom_icon('scroll_down')
.bash.sticky.js-scroll-container .bash.sticky.js-scroll-container
%code.js-build-output %code.js-build-output
.build-loader-animation.js-build-refresh .build-loader-animation.js-build-refresh
   
= render "sidebar" = render "sidebar"
   
.js-build-options{ data: javascript_build_options } .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