Commit 5ed91cf8 authored by Filipa Lacerda's avatar Filipa Lacerda Committed by Phil Hughes

Resolve "Integrate new vue+vuex code base with new API and remove old haml code"

parent 712f41e1
import $ from 'jquery';
import _ from 'underscore';
import { polyfillSticky } from './lib/utils/sticky';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
import { isScrolledToBottom, scrollDown, scrollUp } from './lib/utils/scroll_utils';
import LogOutputBehaviours from './lib/utils/logoutput_behaviours';
export default class Job extends LogOutputBehaviours {
constructor(options) {
super();
this.timeout = null;
this.state = null;
this.fetchingStatusFavicon = false;
this.options = options || $('.js-build-options').data();
this.pagePath = this.options.pagePath;
this.buildStatus = this.options.buildStatus;
this.state = this.options.logState;
this.buildStage = this.options.buildStage;
this.$document = $(document);
this.$window = $(window);
this.logBytes = 0;
this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh');
this.$truncatedInfo = $('.js-truncated-info');
this.$buildTraceOutput = $('.js-build-output');
this.$topBar = $('.js-top-bar');
clearTimeout(this.timeout);
this.initSidebar();
this.sidebarOnResize();
this.$document
.off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window.off('scroll').on('scroll', () => {
if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false);
} else if (isScrolledToBottom() && !this.isLogComplete) {
this.toggleScrollAnimation(true);
}
this.scrollThrottled();
});
this.$window
.off('resize.build')
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
this.initAffixTopArea();
this.getBuildTrace();
}
initAffixTopArea() {
polyfillSticky(this.$topBar);
}
scrollToBottom() {
scrollDown();
this.hasBeenScrolled = true;
this.toggleScroll();
}
scrollToTop() {
scrollUp();
this.hasBeenScrolled = true;
this.toggleScroll();
}
toggleScrollAnimation(toggle) {
this.$scrollBottomBtn.toggleClass('animate', toggle);
}
initSidebar() {
this.$sidebar = $('.js-build-sidebar');
}
getBuildTrace() {
return axios
.get(`${this.pagePath}/trace.json`, {
params: { state: this.state },
})
.then(res => {
const log = res.data;
if (!this.fetchingStatusFavicon) {
this.fetchingStatusFavicon = true;
setCiStatusFavicon(`${this.pagePath}/status.json`)
.then(() => {
this.fetchingStatusFavicon = false;
})
.catch(() => {
this.fetchingStatusFavicon = false;
});
}
if (log.state) {
this.state = log.state;
}
this.isScrollInBottom = isScrolledToBottom();
if (log.append) {
this.$buildTraceOutput.append(log.html);
this.logBytes += log.size;
} else {
this.$buildTraceOutput.html(log.html);
this.logBytes = log.size;
}
// if the incremental sum of logBytes we received is less than the total
// we need to show a message warning the user about that.
if (this.logBytes < log.total) {
// size is in bytes, we need to calculate KiB
const size = numberToHumanSize(this.logBytes);
$('.js-truncated-info-size').html(`${size}`);
this.$truncatedInfo.removeClass('hidden');
} else {
this.$truncatedInfo.addClass('hidden');
}
this.isLogComplete = log.complete;
if (log.complete === false) {
this.timeout = setTimeout(() => {
this.getBuildTrace();
}, 4000);
} else {
this.$buildRefreshAnimation.remove();
this.toggleScrollAnimation(false);
}
if (log.status !== this.buildStatus) {
visitUrl(this.pagePath);
}
})
.catch(() => {
this.$buildRefreshAnimation.remove();
})
.then(() => {
if (this.isScrollInBottom) {
scrollDown();
}
})
.then(() => this.toggleScroll());
}
// eslint-disable-next-line class-methods-use-this
shouldHideSidebarForViewport() {
const bootstrapBreakpoint = bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs';
}
toggleSidebar(shouldHide) {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
const $toggleButton = $('.js-sidebar-build-toggle-header');
this.$sidebar
.toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide);
this.$topBar
.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
if (this.$sidebar.hasClass('right-sidebar-expanded')) {
$toggleButton.addClass('hidden');
} else {
$toggleButton.removeClass('hidden');
}
}
sidebarOnResize() {
this.toggleSidebar(this.shouldHideSidebarForViewport());
}
sidebarOnClick() {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
}
}
<script> <script>
import { mapGetters, mapState } from 'vuex'; import _ from 'underscore';
import { mapGetters, mapState, mapActions } from 'vuex';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import bp from '~/breakpoints';
import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue'; import Callout from '~/vue_shared/components/callout.vue';
import createStore from '../store';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue'; import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue'; import ErasedBlock from './erased_block.vue';
import Log from './job_log.vue';
import LogTopBar from './job_log_controllers.vue';
import StuckBlock from './stuck_block.vue'; import StuckBlock from './stuck_block.vue';
import Sidebar from './sidebar.vue';
export default { export default {
name: 'JobPageApp', name: 'JobPageApp',
store: createStore(),
components: { components: {
CiHeader, CiHeader,
Callout, Callout,
EmptyState, EmptyState,
EnvironmentsBlock, EnvironmentsBlock,
ErasedBlock, ErasedBlock,
Log,
LogTopBar,
StuckBlock, StuckBlock,
Sidebar,
}, },
props: { props: {
runnerSettingsUrl: { runnerSettingsUrl: {
...@@ -23,9 +34,43 @@ ...@@ -23,9 +34,43 @@
required: false, required: false,
default: null, default: null,
}, },
runnerHelpUrl: {
type: String,
required: false,
default: null,
},
endpoint: {
type: String,
required: true,
},
terminalPath: {
type: String,
required: false,
default: null,
},
pagePath: {
type: String,
required: true,
},
logState: {
type: String,
required: true,
},
}, },
computed: { computed: {
...mapState(['isLoading', 'job']), ...mapState([
'isLoading',
'job',
'isSidebarOpen',
'trace',
'isTraceComplete',
'traceSize',
'isTraceSizeVisible',
'isScrollBottomDisabled',
'isScrollTopDisabled',
'isScrolledToBottomBeforeReceivingTrace',
'hasError',
]),
...mapGetters([ ...mapGetters([
'headerActions', 'headerActions',
'headerTime', 'headerTime',
...@@ -35,7 +80,83 @@ ...@@ -35,7 +80,83 @@
'isJobStuck', 'isJobStuck',
'hasTrace', 'hasTrace',
'emptyStateIllustration', 'emptyStateIllustration',
'isScrollingDown',
'emptyStateAction',
]), ]),
shouldRenderContent() {
return !this.isLoading && !this.hasError;
}
},
watch: {
// Once the job log is loaded,
// fetch the stages for the dropdown on the sidebar
job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
this.fetchStages();
}
},
},
created() {
this.throttled = _.throttle(this.toggleScrollButtons, 100);
this.setJobEndpoint(this.endpoint);
this.setTraceOptions({
logState: this.logState,
pagePath: this.pagePath,
});
this.fetchJob();
this.fetchTrace();
window.addEventListener('resize', this.onResize);
window.addEventListener('scroll', this.updateScroll);
},
mounted() {
this.updateSidebar();
},
destroyed() {
window.removeEventListener('resize', this.onResize);
window.removeEventListener('scroll', this.updateScroll);
},
methods: {
...mapActions([
'setJobEndpoint',
'setTraceOptions',
'fetchJob',
'fetchStages',
'hideSidebar',
'showSidebar',
'toggleSidebar',
'fetchTrace',
'scrollBottom',
'scrollTop',
'toggleScrollButtons',
'toggleScrollAnimation',
]),
onResize() {
this.updateSidebar();
this.updateScroll();
},
updateSidebar() {
if (bp.getBreakpointSize() === 'xs') {
this.hideSidebar();
} else if (!this.isSidebarOpen) {
this.showSidebar();
}
},
updateScroll() {
if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false);
} else if (this.isScrollingDown) {
this.toggleScrollAnimation(true);
}
this.throttled();
},
}, },
}; };
</script> </script>
...@@ -44,71 +165,107 @@ ...@@ -44,71 +165,107 @@
<gl-loading-icon <gl-loading-icon
v-if="isLoading" v-if="isLoading"
:size="2" :size="2"
class="prepend-top-20" class="js-job-loading prepend-top-20"
/> />
<template v-else> <template v-else-if="shouldRenderContent">
<!-- Header Section --> <div class="js-job-content build-page">
<header> <!-- Header Section -->
<div class="js-build-header build-header top-area"> <header>
<ci-header <div class="js-build-header build-header top-area">
:status="job.status" <ci-header
:item-id="job.id" :status="job.status"
:time="headerTime" :item-id="job.id"
:user="job.user" :time="headerTime"
:actions="headerActions" :user="job.user"
:has-sidebar-button="true" :actions="headerActions"
:should-render-triggered-label="shouldRenderTriggeredLabel" :has-sidebar-button="true"
:item-name="__('Job')" :should-render-triggered-label="shouldRenderTriggeredLabel"
:item-name="__('Job')"
@clickedSidebarButton="toggleSidebar"
/>
</div>
<callout
v-if="shouldRenderCalloutMessage"
:message="job.callout_message"
/>
</header>
<!-- EO Header Section -->
<!-- Body Section -->
<stuck-block
v-if="isJobStuck"
class="js-job-stuck"
:has-no-runners-for-project="job.runners.available"
:tags="job.tags"
:runners-path="runnerSettingsUrl"
/>
<environments-block
v-if="hasEnvironment"
class="js-job-environment"
:deployment-status="job.deployment_status"
:icon-status="job.status"
/>
<erased-block
v-if="job.erased_at"
class="js-job-erased-block"
:user="job.erased_by"
:erased-at="job.erased_at"
/>
<!--job log -->
<div
v-if="hasTrace"
class="build-trace-container prepend-top-default">
<log-top-bar
:class="{
'sidebar-expanded': isSidebarOpen,
'sidebar-collapsed': !isSidebarOpen
}"
:erase-path="job.erase_path"
:size="traceSize"
:raw-path="job.raw_path"
:is-scroll-bottom-disabled="isScrollBottomDisabled"
:is-scroll-top-disabled="isScrollTopDisabled"
:is-trace-size-visible="isTraceSizeVisible"
:is-scrolling-down="isScrollingDown"
@scrollJobLogTop="scrollTop"
@scrollJobLogBottom="scrollBottom"
/>
<log
:trace="trace"
:is-complete="isTraceComplete"
/> />
</div> </div>
<!-- EO job log -->
<callout <!--empty state -->
v-if="shouldRenderCalloutMessage" <empty-state
:message="job.callout_message" v-if="!hasTrace"
class="js-job-empty-state"
:illustration-path="emptyStateIllustration.image"
:illustration-size-class="emptyStateIllustration.size"
:title="emptyStateIllustration.title"
:content="emptyStateIllustration.content"
:action="emptyStateAction"
/> />
</header>
<!-- EO Header Section -->
<!-- Body Section -->
<stuck-block
v-if="isJobStuck"
class="js-job-stuck"
:has-no-runners-for-project="job.runners.available"
:tags="job.tags"
:runners-path="runnerSettingsUrl"
/>
<environments-block
v-if="hasEnvironment"
class="js-job-environment"
:deployment-status="job.deployment_status"
:icon-status="job.status"
/>
<erased-block
v-if="job.erased_at"
class="js-job-erased-block"
:user="job.erased_by"
:erased-at="job.erased_at"
/>
<!--job log -->
<!-- EO job log -->
<!--empty state -->
<empty-state
v-if="!hasTrace"
class="js-job-empty-state"
:illustration-path="emptyStateIllustration.image"
:illustration-size-class="emptyStateIllustration.size"
:title="emptyStateIllustration.title"
:content="emptyStateIllustration.content"
:action="job.status.action"
/>
<!-- EO empty state --> <!-- EO empty state -->
<!-- EO Body Section --> <!-- EO Body Section -->
</div>
</template> </template>
<sidebar
v-if="shouldRenderContent"
class="js-job-sidebar"
:class="{
'right-sidebar-expanded': isSidebarOpen,
'right-sidebar-collapsed': !isSidebarOpen
}"
:runner-help-url="runnerHelpUrl"
/>
</div> </div>
</template> </template>
<script> <script>
export default { import { mapState, mapActions } from 'vuex';
name: 'JobLog',
props: { export default {
trace: { name: 'JobLog',
type: String, props: {
required: true, trace: {
type: String,
required: true,
},
isComplete: {
type: Boolean,
required: true,
},
},
computed: {
...mapState(['isScrolledToBottomBeforeReceivingTrace']),
},
updated() {
this.$nextTick(() => this.handleScrollDown());
},
mounted() {
this.$nextTick(() => this.handleScrollDown());
}, },
isComplete: { methods: {
type: Boolean, ...mapActions(['scrollBottom']),
required: true, /**
* The job log is sent in HTML, which means we need to use `v-html` to render it
* Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
* in this case because it runs before `v-html` has finished running, since there's no
* Vue binding.
* In order to scroll the page down after `v-html` has finished, we need to use setTimeout
*/
handleScrollDown() {
if (this.isScrolledToBottomBeforeReceivingTrace) {
setTimeout(() => {
this.scrollBottom();
}, 0);
}
},
}, },
}, };
};
</script> </script>
<template> <template>
<pre class="build-trace"> <pre class="js-build-trace build-trace">
<code <code
class="bash" class="bash"
v-html="trace" v-html="trace"
...@@ -22,7 +50,7 @@ export default { ...@@ -22,7 +50,7 @@ export default {
</code> </code>
<div <div
v-if="isComplete" v-if="!isComplete"
class="js-log-animation build-loader-animation" class="js-log-animation build-loader-animation"
> >
<div class="dot"></div> <div class="dot"></div>
......
...@@ -4,6 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -4,6 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import scrollDown from '../svg/scroll_down.svg';
export default { export default {
components: { components: {
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
directives: { directives: {
tooltip, tooltip,
}, },
scrollDown,
props: { props: {
erasePath: { erasePath: {
type: String, type: String,
...@@ -65,7 +67,7 @@ export default { ...@@ -65,7 +67,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="top-bar affix js-top-bar"> <div class="top-bar affix">
<!-- truncate information --> <!-- truncate information -->
<div class="js-truncated-info truncated-info d-none d-sm-block float-left"> <div class="js-truncated-info truncated-info d-none d-sm-block float-left">
<template v-if="isTraceSizeVisible"> <template v-if="isTraceSizeVisible">
...@@ -100,7 +102,7 @@ export default { ...@@ -100,7 +102,7 @@ export default {
v-tooltip v-tooltip
:title="s__('Job|Erase job log')" :title="s__('Job|Erase job log')"
:href="erasePath" :href="erasePath"
data-confirm="__('Are you sure you want to erase this build?')" :data-confirm="__('Are you sure you want to erase this build?')"
class="js-erase-link controllers-buttons" class="js-erase-link controllers-buttons"
data-container="body" data-container="body"
data-method="post" data-method="post"
...@@ -138,8 +140,8 @@ export default { ...@@ -138,8 +140,8 @@ export default {
class="js-scroll-bottom btn-scroll btn-transparent btn-blank" class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
:class="{ animate: isScrollingDown }" :class="{ animate: isScrollingDown }"
@click="handleScrollToBottom" @click="handleScrollToBottom"
v-html="$options.scrollDown"
> >
<icon name="scroll_down"/>
</button> </button>
</div> </div>
<!-- eo scroll buttons --> <!-- eo scroll buttons -->
......
...@@ -29,14 +29,9 @@ export default { ...@@ -29,14 +29,9 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
terminalPath: {
type: String,
required: false,
default: null,
},
}, },
computed: { computed: {
...mapState(['job', 'isLoading', 'stages', 'jobs', 'selectedStage']), ...mapState(['job', 'stages', 'jobs', 'selectedStage']),
coverage() { coverage() {
return `${this.job.coverage}%`; return `${this.job.coverage}%`;
}, },
...@@ -64,10 +59,10 @@ export default { ...@@ -64,10 +59,10 @@ export default {
return ''; return '';
} }
let t = this.job.metadata.timeout_human_readable; let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') { if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`; t += ` (from ${this.job.metadata.timeout_source})`;
} }
return t; return t;
}, },
...@@ -100,196 +95,190 @@ export default { ...@@ -100,196 +95,190 @@ export default {
); );
}, },
commit() { commit() {
return this.job.pipeline.commit || {}; return this.job.pipeline && this.job.pipeline.commit ? this.job.pipeline.commit : {};
}, },
}, },
methods: { methods: {
...mapActions(['fetchJobsForStage']), ...mapActions(['fetchJobsForStage', 'toggleSidebar']),
}, },
}; };
</script> </script>
<template> <template>
<aside <aside
class="js-build-sidebar right-sidebar right-sidebar-expanded build-sidebar" class="right-sidebar build-sidebar"
data-offset-top="101" data-offset-top="101"
data-spy="affix" data-spy="affix"
> >
<div class="sidebar-container"> <div class="sidebar-container">
<div class="blocks-container"> <div class="blocks-container">
<template v-if="!isLoading"> <div class="block">
<div class="block"> <strong class="inline prepend-top-8">
<strong class="inline prepend-top-8"> {{ job.name }}
{{ job.name }} </strong>
</strong> <a
<a v-if="job.retry_path"
v-if="job.retry_path" :class="retryButtonClass"
:class="retryButtonClass" :href="job.retry_path"
:href="job.retry_path" data-method="post"
data-method="post" rel="nofollow"
rel="nofollow"
>
{{ __('Retry') }}
</a>
<a
v-if="terminalPath"
:href="terminalPath"
class="js-terminal-link pull-right btn btn-primary
btn-inverted visible-md-block visible-lg-block"
target="_blank"
>
{{ __('Debug') }}
<icon name="external-link" />
</a>
<button
:aria-label="__('Toggle Sidebar')"
type="button"
class="btn btn-blank gutter-toggle
float-right d-block d-md-none js-sidebar-build-toggle"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-angle-double-right"
></i>
</button>
</div>
<div
v-if="job.retry_path || job.new_issue_path"
class="block retry-link"
> >
<a {{ __('Retry') }}
v-if="job.new_issue_path" </a>
:href="job.new_issue_path" <a
class="js-new-issue btn btn-success btn-inverted" v-if="job.terminal_path"
> :href="job.terminal_path"
{{ __('New issue') }} class="js-terminal-link pull-right btn btn-primary
btn-inverted visible-md-block visible-lg-block"
target="_blank"
>
{{ __('Debug') }}
<icon name="external-link" />
</a>
<button
:aria-label="__('Toggle Sidebar')"
type="button"
class="btn btn-blank gutter-toggle
float-right d-block d-md-none js-sidebar-build-toggle"
@click="toggleSidebar"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-angle-double-right"
></i>
</button>
</div>
<div
v-if="job.retry_path || job.new_issue_path"
class="block retry-link"
>
<a
v-if="job.new_issue_path"
:href="job.new_issue_path"
class="js-new-issue btn btn-success btn-inverted"
>
{{ __('New issue') }}
</a>
<a
v-if="job.retry_path"
:href="job.retry_path"
class="js-retry-job btn btn-inverted-secondary"
data-method="post"
rel="nofollow"
>
{{ __('Retry') }}
</a>
</div>
<div :class="{ block : renderBlock }">
<p
v-if="job.merge_request"
class="build-detail-row js-job-mr"
>
<span class="build-light-text">
{{ __('Merge Request:') }}
</span>
<a :href="job.merge_request.path">
!{{ job.merge_request.iid }}
</a> </a>
</p>
<detail-row
v-if="job.duration"
:value="duration"
class="js-job-duration"
title="Duration"
/>
<detail-row
v-if="job.finished_at"
:value="timeFormated(job.finished_at)"
class="js-job-finished"
title="Finished"
/>
<detail-row
v-if="job.erased_at"
:value="timeFormated(job.erased_at)"
class="js-job-erased"
title="Erased"
/>
<detail-row
v-if="job.queued"
:value="queued"
class="js-job-queued"
title="Queued"
/>
<detail-row
v-if="hasTimeout"
:help-url="runnerHelpUrl"
:value="timeout"
class="js-job-timeout"
title="Timeout"
/>
<detail-row
v-if="job.runner"
:value="runnerId"
class="js-job-runner"
title="Runner"
/>
<detail-row
v-if="job.coverage"
:value="coverage"
class="js-job-coverage"
title="Coverage"
/>
<p
v-if="job.tags.length"
class="build-detail-row js-job-tags"
>
<span class="build-light-text">
{{ __('Tags:') }}
</span>
<span
v-for="(tag, i) in job.tags"
:key="i"
class="label label-primary">
{{ tag }}
</span>
</p>
<div
v-if="job.cancel_path"
class="btn-group prepend-top-5"
role="group">
<a <a
v-if="job.retry_path" :href="job.cancel_path"
:href="job.retry_path" class="js-cancel-job btn btn-sm btn-default"
class="js-retry-job btn btn-inverted-secondary"
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
> >
{{ __('Retry') }} {{ __('Cancel') }}
</a> </a>
</div> </div>
<div :class="{ block : renderBlock }"> </div>
<p
v-if="job.merge_request"
class="build-detail-row js-job-mr"
>
<span class="build-light-text">
{{ __('Merge Request:') }}
</span>
<a :href="job.merge_request.path">
!{{ job.merge_request.iid }}
</a>
</p>
<detail-row
v-if="job.duration"
:value="duration"
class="js-job-duration"
title="Duration"
/>
<detail-row
v-if="job.finished_at"
:value="timeFormated(job.finished_at)"
class="js-job-finished"
title="Finished"
/>
<detail-row
v-if="job.erased_at"
:value="timeFormated(job.erased_at)"
class="js-job-erased"
title="Erased"
/>
<detail-row
v-if="job.queued"
:value="queued"
class="js-job-queued"
title="Queued"
/>
<detail-row
v-if="hasTimeout"
:help-url="runnerHelpUrl"
:value="timeout"
class="js-job-timeout"
title="Timeout"
/>
<detail-row
v-if="job.runner"
:value="runnerId"
class="js-job-runner"
title="Runner"
/>
<detail-row
v-if="job.coverage"
:value="coverage"
class="js-job-coverage"
title="Coverage"
/>
<p
v-if="job.tags.length"
class="build-detail-row js-job-tags"
>
<span class="build-light-text">
{{ __('Tags:') }}
</span>
<span
v-for="(tag, i) in job.tags"
:key="i"
class="label label-primary">
{{ tag }}
</span>
</p>
<div
v-if="job.cancel_path"
class="btn-group prepend-top-5"
role="group">
<a
:href="job.cancel_path"
class="js-cancel-job btn btn-sm btn-default"
data-method="post"
rel="nofollow"
>
{{ __('Cancel') }}
</a>
</div>
</div>
<artifacts-block
v-if="hasArtifact"
:artifact="job.artifact"
/>
<trigger-block
v-if="hasTriggers"
:trigger="job.trigger"
/>
<commit-block
:is-last-block="hasStages"
:commit="commit"
:merge-request="job.merge_request"
/>
<stages-dropdown <artifacts-block
:stages="stages" v-if="hasArtifact"
:pipeline="job.pipeline" :artifact="job.artifact"
:selected-stage="selectedStage" />
@requestSidebarStageDropdown="fetchJobsForStage" <trigger-block
/> v-if="hasTriggers"
:trigger="job.trigger"
/>
<commit-block
:is-last-block="hasStages"
:commit="commit"
:merge-request="job.merge_request"
/>
</template> <stages-dropdown
<gl-loading-icon :stages="stages"
v-else :pipeline="job.pipeline"
:size="2" :selected-stage="selectedStage"
class="prepend-top-10" @requestSidebarStageDropdown="fetchJobsForStage"
/> />
</div> </div>
<jobs-container <jobs-container
v-if="!isLoading && jobs.length" v-if="jobs.length"
:jobs="jobs" :jobs="jobs"
:job-id="job.id" :job-id="job.id"
/> />
......
import Vue from 'vue';
import JobApp from './components/job_app.vue';
export default () => {
const element = document.getElementById('js-job-vue-app');
return new Vue({
el: element,
components: {
JobApp,
},
render(createElement) {
return createElement('job-app', {
props: {
runnerHelpUrl: element.dataset.runnerHelpUrl,
runnerSettingsUrl: element.dataset.runnerSettingsUrl,
endpoint: element.dataset.endpoint,
pagePath: element.dataset.buildOptionsPagePath,
logState: element.dataset.buildOptionsLogState,
buildStatus: element.dataset.buildOptionsBuildStatus,
},
});
},
});
};
import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
import Vue from 'vue';
import Job from '../job';
import JobApp from './components/job_app.vue';
import Sidebar from './components/sidebar.vue';
import createStore from './store';
export default () => {
const { dataset } = document.getElementById('js-job-details-vue');
const store = createStore();
store.dispatch('setJobEndpoint', dataset.endpoint);
store.dispatch('fetchJob');
// Header
// eslint-disable-next-line no-new
new Vue({
el: '#js-build-header-vue',
components: {
JobApp,
},
store,
computed: {
...mapState(['job', 'isLoading']),
},
render(createElement) {
return createElement('job-app', {
props: {
isLoading: this.isLoading,
job: this.job,
runnerSettingsUrl: dataset.runnerSettingsUrl,
},
});
},
});
// Sidebar information block
const detailsBlockElement = document.getElementById('js-details-block-vue');
const detailsBlockDataset = detailsBlockElement.dataset;
// eslint-disable-next-line
new Vue({
el: detailsBlockElement,
components: {
Sidebar,
},
computed: {
...mapState(['job']),
},
watch: {
job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
this.fetchStages();
}
},
},
methods: {
...mapActions(['fetchStages']),
},
store,
render(createElement) {
return createElement('sidebar', {
props: {
runnerHelpUrl: dataset.runnerHelpUrl,
terminalPath: detailsBlockDataset.terminalPath,
},
});
},
});
// eslint-disable-next-line no-new
new Job();
};
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import * as types from './mutation_types'; import * as types from './mutation_types';
import axios from '../../lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Poll from '../../lib/utils/poll'; import Poll from '~/lib/utils/poll';
import { setCiStatusFavicon } from '../../lib/utils/common_utils'; import { setFaviconOverlay, resetFavicon } from '~/lib/utils/common_utils';
import flash from '../../flash'; import flash from '~/flash';
import { __ } from '../../locale'; import { __ } from '~/locale';
import {
canScroll,
isScrolledToBottom,
isScrolledToTop,
isScrolledToMiddle,
scrollDown,
scrollUp,
} from '~/lib/utils/scroll_utils';
export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint); export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint);
export const setTraceEndpoint = ({ commit }, endpoint) => export const setTraceOptions = ({ commit }, options) => commit(types.SET_TRACE_OPTIONS, options);
commit(types.SET_TRACE_ENDPOINT, endpoint);
export const setStagesEndpoint = ({ commit }, endpoint) => export const hideSidebar = ({ commit }) => commit(types.HIDE_SIDEBAR);
commit(types.SET_STAGES_ENDPOINT, endpoint); export const showSidebar = ({ commit }) => commit(types.SHOW_SIDEBAR);
export const setJobsEndpoint = ({ commit }, endpoint) => commit(types.SET_JOBS_ENDPOINT, endpoint);
export const toggleSidebar = ({ dispatch, state }) => {
if (state.isSidebarOpen) {
dispatch('hideSidebar');
} else {
dispatch('showSidebar');
}
};
let eTagPoll; let eTagPoll;
...@@ -62,41 +77,84 @@ export const fetchJob = ({ state, dispatch }) => { ...@@ -62,41 +77,84 @@ export const fetchJob = ({ state, dispatch }) => {
}); });
}; };
export const receiveJobSuccess = ({ commit }, data) => { export const receiveJobSuccess = ({ commit }, data = {}) => {
commit(types.RECEIVE_JOB_SUCCESS, data); commit(types.RECEIVE_JOB_SUCCESS, data);
if (data.favicon) {
setFaviconOverlay(data.favicon);
} else {
resetFavicon();
}
}; };
export const receiveJobError = ({ commit }) => { export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR); commit(types.RECEIVE_JOB_ERROR);
flash(__('An error occurred while fetching the job.')); flash(__('An error occurred while fetching the job.'));
resetFavicon();
}; };
/** /**
* Job's Trace * Job's Trace
*/ */
export const scrollTop = ({ commit }) => { export const scrollTop = ({ dispatch }) => {
commit(types.SCROLL_TO_TOP); scrollUp();
window.scrollTo({ top: 0 }); dispatch('toggleScrollButtons');
}; };
export const scrollBottom = ({ commit }) => { export const scrollBottom = ({ dispatch }) => {
commit(types.SCROLL_TO_BOTTOM); scrollDown();
window.scrollTo({ top: document.height }); dispatch('toggleScrollButtons');
};
/**
* Responsible for toggling the disabled state of the scroll buttons
*/
export const toggleScrollButtons = ({ dispatch }) => {
if (canScroll()) {
if (isScrolledToMiddle()) {
dispatch('enableScrollTop');
dispatch('enableScrollBottom');
} else if (isScrolledToTop()) {
dispatch('disableScrollTop');
dispatch('enableScrollBottom');
} else if (isScrolledToBottom()) {
dispatch('disableScrollBottom');
dispatch('enableScrollTop');
}
} else {
dispatch('disableScrollBottom');
dispatch('disableScrollTop');
}
};
export const disableScrollBottom = ({ commit }) => commit(types.DISABLE_SCROLL_BOTTOM);
export const disableScrollTop = ({ commit }) => commit(types.DISABLE_SCROLL_TOP);
export const enableScrollBottom = ({ commit }) => commit(types.ENABLE_SCROLL_BOTTOM);
export const enableScrollTop = ({ commit }) => commit(types.ENABLE_SCROLL_TOP);
/**
* While the automatic scroll down is active,
* we show the scroll down button with an animation
*/
export const toggleScrollAnimation = ({ commit }, toggle) =>
commit(types.TOGGLE_SCROLL_ANIMATION, toggle);
/**
* Responsible to handle automatic scroll
*/
export const toggleScrollisInBottom = ({ commit }, toggle) => {
commit(types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE, toggle);
}; };
export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE); export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE);
let traceTimeout; let traceTimeout;
export const fetchTrace = ({ dispatch, state }) => { export const fetchTrace = ({ dispatch, state }) =>
dispatch('requestTrace');
axios axios
.get(`${state.traceEndpoint}/trace.json`, { .get(`${state.traceEndpoint}/trace.json`, {
params: { state: state.traceState }, params: { state: state.traceState },
}) })
.then(({ data }) => { .then(({ data }) => {
if (!state.fetchingStatusFavicon) { dispatch('toggleScrollisInBottom', isScrolledToBottom());
dispatch('fetchFavicon');
}
dispatch('receiveTraceSuccess', data); dispatch('receiveTraceSuccess', data);
if (!data.complete) { if (!data.complete) {
...@@ -108,7 +166,7 @@ export const fetchTrace = ({ dispatch, state }) => { ...@@ -108,7 +166,7 @@ export const fetchTrace = ({ dispatch, state }) => {
} }
}) })
.catch(() => dispatch('receiveTraceError')); .catch(() => dispatch('receiveTraceError'));
};
export const stopPollingTrace = ({ commit }) => { export const stopPollingTrace = ({ commit }) => {
commit(types.STOP_POLLING_TRACE); commit(types.STOP_POLLING_TRACE);
clearTimeout(traceTimeout); clearTimeout(traceTimeout);
...@@ -120,17 +178,6 @@ export const receiveTraceError = ({ commit }) => { ...@@ -120,17 +178,6 @@ export const receiveTraceError = ({ commit }) => {
flash(__('An error occurred while fetching the job log.')); flash(__('An error occurred while fetching the job log.'));
}; };
export const fetchFavicon = ({ state, dispatch }) => {
dispatch('requestStatusFavicon');
setCiStatusFavicon(`${state.pagePath}/status.json`)
.then(() => dispatch('receiveStatusFaviconSuccess'))
.catch(() => dispatch('requestStatusFaviconError'));
};
export const requestStatusFavicon = ({ commit }) => commit(types.REQUEST_STATUS_FAVICON);
export const receiveStatusFaviconSuccess = ({ commit }) =>
commit(types.RECEIVE_STATUS_FAVICON_SUCCESS);
export const requestStatusFaviconError = ({ commit }) => commit(types.RECEIVE_STATUS_FAVICON_ERROR);
/** /**
* Stages dropdown on sidebar * Stages dropdown on sidebar
*/ */
......
import _ from 'underscore'; import _ from 'underscore';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
export const headerActions = state => { export const headerActions = state => {
if (state.job.new_issue_path) { if (state.job.new_issue_path) {
...@@ -34,11 +35,12 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); ...@@ -34,11 +35,12 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
* Used to check if it should render the job log or the empty state * Used to check if it should render the job log or the empty state
* @returns {Boolean} * @returns {Boolean}
*/ */
export const hasTrace = state => state.job.has_trace || state.job.status.group === 'running'; export const hasTrace = state => state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running');
export const emptyStateIllustration = state => export const emptyStateIllustration = state =>
(state.job && state.job.status && state.job.status.illustration) || {}; (state.job && state.job.status && state.job.status.illustration) || {};
export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {};
/** /**
* When the job is pending and there are no available runners * When the job is pending and there are no available runners
* we need to render the stuck block; * we need to render the stuck block;
...@@ -46,8 +48,10 @@ export const emptyStateIllustration = state => ...@@ -46,8 +48,10 @@ export const emptyStateIllustration = state =>
* @returns {Boolean} * @returns {Boolean}
*/ */
export const isJobStuck = state => export const isJobStuck = state =>
state.job.status.group === 'pending' && (!_.isEmpty(state.job.status) && state.job.status.group === 'pending') &&
(!_.isEmpty(state.job.runners) && state.job.runners.available === false); (!_.isEmpty(state.job.runners) && state.job.runners.available === false);
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT'; export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT';
export const SET_TRACE_ENDPOINT = 'SET_TRACE_ENDPOINT'; export const SET_TRACE_OPTIONS = 'SET_TRACE_OPTIONS';
export const SET_STAGES_ENDPOINT = 'SET_STAGES_ENDPOINT';
export const SET_JOBS_ENDPOINT = 'SET_JOBS_ENDPOINT'; export const HIDE_SIDEBAR = 'HIDE_SIDEBAR';
export const SHOW_SIDEBAR = 'SHOW_SIDEBAR';
export const SCROLL_TO_TOP = 'SCROLL_TO_TOP'; export const SCROLL_TO_TOP = 'SCROLL_TO_TOP';
export const SCROLL_TO_BOTTOM = 'SCROLL_TO_BOTTOM'; export const SCROLL_TO_BOTTOM = 'SCROLL_TO_BOTTOM';
export const DISABLE_SCROLL_BOTTOM = 'DISABLE_SCROLL_BOTTOM';
export const DISABLE_SCROLL_TOP = 'DISABLE_SCROLL_TOP';
export const ENABLE_SCROLL_BOTTOM = 'ENABLE_SCROLL_BOTTOM';
export const ENABLE_SCROLL_TOP = 'ENABLE_SCROLL_TOP';
// TODO
export const TOGGLE_SCROLL_ANIMATION = 'TOGGLE_SCROLL_ANIMATION';
export const TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE = 'TOGGLE_IS_SCROLL_IN_BOTTOM';
export const REQUEST_JOB = 'REQUEST_JOB'; export const REQUEST_JOB = 'REQUEST_JOB';
export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS'; export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS';
...@@ -15,10 +24,6 @@ export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE'; ...@@ -15,10 +24,6 @@ export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE';
export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS'; export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS';
export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR'; export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR';
export const REQUEST_STATUS_FAVICON = 'REQUEST_STATUS_FAVICON';
export const RECEIVE_STATUS_FAVICON_SUCCESS = 'RECEIVE_STATUS_FAVICON_SUCCESS';
export const RECEIVE_STATUS_FAVICON_ERROR = 'RECEIVE_STATUS_FAVICON_ERROR';
export const REQUEST_STAGES = 'REQUEST_STAGES'; export const REQUEST_STAGES = 'REQUEST_STAGES';
export const RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS'; export const RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS';
export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR'; export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR';
......
...@@ -4,14 +4,17 @@ export default { ...@@ -4,14 +4,17 @@ export default {
[types.SET_JOB_ENDPOINT](state, endpoint) { [types.SET_JOB_ENDPOINT](state, endpoint) {
state.jobEndpoint = endpoint; state.jobEndpoint = endpoint;
}, },
[types.REQUEST_STATUS_FAVICON](state) {
state.fetchingStatusFavicon = true; [types.SET_TRACE_OPTIONS](state, options = {}) {
state.traceEndpoint = options.pagePath;
state.traceState = options.logState;
}, },
[types.RECEIVE_STATUS_FAVICON_SUCCESS](state) {
state.fetchingStatusFavicon = false; [types.HIDE_SIDEBAR](state) {
state.isSidebarOpen = false;
}, },
[types.RECEIVE_STATUS_FAVICON_ERROR](state) { [types.SHOW_SIDEBAR](state) {
state.fetchingStatusFavicon = false; state.isSidebarOpen = true;
}, },
[types.RECEIVE_TRACE_SUCCESS](state, log) { [types.RECEIVE_TRACE_SUCCESS](state, log) {
...@@ -23,8 +26,12 @@ export default { ...@@ -23,8 +26,12 @@ export default {
state.trace += log.html; state.trace += log.html;
state.traceSize += log.size; state.traceSize += log.size;
} else { } else {
state.trace = log.html; // When the job still does not have a trace
state.traceSize = log.size; // the trace response will not have a defined
// html or size. We keep the old value otherwise these
// will be set to `undefined`
state.trace = log.html || state.trace;
state.traceSize = log.size || state.traceSize;
} }
if (state.traceSize < log.total) { if (state.traceSize < log.total) {
...@@ -33,25 +40,29 @@ export default { ...@@ -33,25 +40,29 @@ export default {
state.isTraceSizeVisible = false; state.isTraceSizeVisible = false;
} }
state.isTraceComplete = log.complete; state.isTraceComplete = log.complete || state.isTraceComplete;
state.hasTraceError = false;
}, },
/**
* Will remove loading animation
*/
[types.STOP_POLLING_TRACE](state) { [types.STOP_POLLING_TRACE](state) {
state.isTraceComplete = true; state.isTraceComplete = true;
}, },
// todo_fl: check this.
/**
* Will remove loading animation
*/
[types.RECEIVE_TRACE_ERROR](state) { [types.RECEIVE_TRACE_ERROR](state) {
state.isLoadingTrace = false;
state.isTraceComplete = true; state.isTraceComplete = true;
state.hasTraceError = true;
}, },
[types.REQUEST_JOB](state) { [types.REQUEST_JOB](state) {
state.isLoading = true; state.isLoading = true;
}, },
[types.RECEIVE_JOB_SUCCESS](state, job) { [types.RECEIVE_JOB_SUCCESS](state, job) {
state.isLoading = false;
state.hasError = false; state.hasError = false;
state.isLoading = false;
state.job = job; state.job = job;
/** /**
...@@ -66,17 +77,28 @@ export default { ...@@ -66,17 +77,28 @@ export default {
}, },
[types.RECEIVE_JOB_ERROR](state) { [types.RECEIVE_JOB_ERROR](state) {
state.isLoading = false; state.isLoading = false;
state.hasError = true;
state.job = {}; state.job = {};
state.hasError = true;
}, },
[types.SCROLL_TO_TOP](state) { [types.ENABLE_SCROLL_TOP](state) {
state.isTraceScrolledToBottom = false; state.isScrollTopDisabled = false;
state.hasBeenScrolled = true; },
[types.DISABLE_SCROLL_TOP](state) {
state.isScrollTopDisabled = true;
},
[types.ENABLE_SCROLL_BOTTOM](state) {
state.isScrollBottomDisabled = false;
}, },
[types.SCROLL_TO_BOTTOM](state) { [types.DISABLE_SCROLL_BOTTOM](state) {
state.isTraceScrolledToBottom = true; state.isScrollBottomDisabled = true;
state.hasBeenScrolled = true; },
[types.TOGGLE_SCROLL_ANIMATION](state, toggle) {
state.isScrollingDown = toggle;
},
[types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE](state, toggle) {
state.isScrolledToBottomBeforeReceivingTrace = toggle;
}, },
[types.REQUEST_STAGES](state) { [types.REQUEST_STAGES](state) {
......
...@@ -4,36 +4,29 @@ export default () => ({ ...@@ -4,36 +4,29 @@ export default () => ({
jobEndpoint: null, jobEndpoint: null,
traceEndpoint: null, traceEndpoint: null,
// dropdown options // sidebar
stagesEndpoint: null, isSidebarOpen: true,
// list of jobs on sidebard
stageJobsEndpoint: null,
// job log
isLoading: false, isLoading: false,
hasError: false, hasError: false,
job: {}, job: {},
// trace // scroll buttons state
isLoadingTrace: false, isScrollBottomDisabled: true,
hasTraceError: false, isScrollTopDisabled: true,
trace: '', // Used to check if we should keep the automatic scroll
isScrolledToBottomBeforeReceivingTrace: true,
isTraceScrolledToBottom: false,
hasBeenScrolled: false,
trace: '',
isTraceComplete: false, isTraceComplete: false,
traceSize: 0, // todo_fl: needs to be converted into human readable format in components traceSize: 0,
isTraceSizeVisible: false, isTraceSizeVisible: false,
fetchingStatusFavicon: false, // used as a query parameter to fetch the trace
// used as a query parameter
traceState: null, traceState: null,
// used to check if we need to redirect the user - todo_fl: check if actually needed
traceStatus: null,
// sidebar dropdown // sidebar dropdown & list of jobs
isLoadingStages: false, isLoadingStages: false,
isLoadingJobs: false, isLoadingJobs: false,
selectedStage: __('More'), selectedStage: __('More'),
......
<svg width="12" height="16" viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path class="first-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043c.124 0 .23-.035.321-.105.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/>
<path class="second-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/>
<path class="third-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91A.458.458 0 0 1 6.257 6h-.37a.626.626 0 0 1-.136-.09"/>
</svg>
import initJobDetails from '~/jobs/job_details_bundle'; import initJobDetails from '~/jobs';
document.addEventListener('DOMContentLoaded', initJobDetails); document.addEventListener('DOMContentLoaded', initJobDetails);
...@@ -69,6 +69,9 @@ export default { ...@@ -69,6 +69,9 @@ export default {
onClickAction(action) { onClickAction(action) {
this.$emit('actionClicked', action); this.$emit('actionClicked', action);
}, },
onClickSidebarButton() {
this.$emit('clickedSidebarButton');
},
}, },
}; };
</script> </script>
...@@ -161,21 +164,21 @@ export default { ...@@ -161,21 +164,21 @@ export default {
</i> </i>
</button> </button>
</template> </template>
<button </section>
v-if="hasSidebarButton" <button
id="toggleSidebar" v-if="hasSidebarButton"
type="button" id="toggleSidebar"
class="btn btn-default d-block d-sm-none d-md-none type="button"
class="btn btn-default d-block d-sm-none
sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
aria-label="Toggle Sidebar" @click="onClickSidebarButton"
>
<i
class="fa fa-angle-double-left"
aria-hidden="true"
aria-labelledby="toggleSidebar"
> >
<i </i>
class="fa fa-angle-double-left" </button>
aria-hidden="true"
aria-labelledby="toggleSidebar"
>
</i>
</button>
</section>
</header> </header>
</template> </template>
...@@ -117,7 +117,6 @@ ...@@ -117,7 +117,6 @@
.controllers { .controllers {
display: flex; display: flex;
font-size: 15px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
...@@ -179,6 +178,7 @@ ...@@ -179,6 +178,7 @@
.build-loader-animation { .build-loader-animation {
@include build-loader-animation; @include build-loader-animation;
float: left;
} }
} }
......
- breadcrumb_title _('Artifacts') - breadcrumb_title _('Artifacts')
- page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' - page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
= render "projects/jobs/header", show_controls: false = render "projects/jobs/header"
- add_to_breadcrumbs(s_('CICD|Jobs'), project_jobs_path(@project)) - add_to_breadcrumbs(s_('CICD|Jobs'), project_jobs_path(@project))
- add_to_breadcrumbs("##{@build.id}", project_jobs_path(@project)) - add_to_breadcrumbs("##{@build.id}", project_jobs_path(@project))
......
- page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' - page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
= render "projects/jobs/header", show_controls: false = render "projects/jobs/header"
.tree-holder .tree-holder
.nav-block .nav-block
......
- show_controls = local_assigns.fetch(:show_controls, true)
- pipeline = @build.pipeline - pipeline = @build.pipeline
.content-block.build-header.top-area.page-content-header .content-block.build-header.top-area.page-content-header
...@@ -20,12 +19,3 @@ ...@@ -20,12 +19,3 @@
= render "projects/jobs/user" if @build.user = render "projects/jobs/user" if @build.user
= time_ago_with_tooltip(@build.created_at) = time_ago_with_tooltip(@build.created_at)
- if show_controls
.nav-controls
- if can?(current_user, :create_issue, @project) && @build.failed?
= link_to "New issue", new_project_issue_path(@project, issue: build_failed_issue_options), class: 'btn btn-success btn-inverted'
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry job", retry_project_job_path(@project, @build), class: 'btn btn-inverted-secondary', method: :post
%button.btn.btn-default.float-right.d-block.d-sm-none.d-md-none.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
- @no_container = true - @no_container = true
- add_to_breadcrumbs "Jobs", project_jobs_path(@project) - add_to_breadcrumbs _("Jobs"), project_jobs_path(@project)
- breadcrumb_title "##{@build.id}" - breadcrumb_title "##{@build.id}"
- page_title "#{@build.name} (##{@build.id})", "Jobs" - page_title "#{@build.name} (##{@build.id})", _("Jobs")
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/xterm' = stylesheet_link_tag 'page_bundles/xterm'
%div{ class: container_class } %div{ class: container_class }
.build-page.js-build-page #js-job-vue-app{ data: { endpoint: project_job_path(@project, @build, format: :json),
#js-build-header-vue runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'),
runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings'),
- if @build.running? || @build.has_trace? build_options: javascript_build_options } }
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
.js-truncated-info.truncated-info.d-none.d-sm-block.float-left.hidden<
Showing last
%span.js-truncated-info-size.truncated-info-size><
of log -
%a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw
.controllers.float-right
- if @build.has_trace?
= link_to raw_project_job_path(@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 @build.erasable? && can?(current_user, :erase_build, @build)
= link_to erase_project_job_path(@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')
= render 'shared/builds/build_output'
#js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
.js-build-options{ data: javascript_build_options }
#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json),
runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'),
runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings') } }
---
title: Transform job page into a single Vue+Vuex application
merge_request:
author:
type: other
...@@ -627,6 +627,9 @@ msgstr "" ...@@ -627,6 +627,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?" msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "" msgstr ""
msgid "Are you sure you want to erase this build?"
msgstr ""
msgid "Are you sure you want to lose unsaved changes?" msgid "Are you sure you want to lose unsaved changes?"
msgstr "" msgstr ""
......
...@@ -67,7 +67,7 @@ describe 'Project Jobs Permissions' do ...@@ -67,7 +67,7 @@ describe 'Project Jobs Permissions' do
it_behaves_like 'recent job page details responds with status', 200 do it_behaves_like 'recent job page details responds with status', 200 do
it 'renders job details', :js do it 'renders job details', :js do
expect(page).to have_content "Job ##{job.id}" expect(page).to have_content "Job ##{job.id}"
expect(page).to have_css '#build-trace' expect(page).to have_css '.js-build-trace'
end end
end end
......
...@@ -20,7 +20,7 @@ describe 'User browses a job', :js do ...@@ -20,7 +20,7 @@ describe 'User browses a job', :js do
wait_for_requests wait_for_requests
expect(page).to have_content("Job ##{build.id}") expect(page).to have_content("Job ##{build.id}")
expect(page).to have_css('#build-trace') expect(page).to have_css('.js-build-trace')
# scroll to the top of the page first # scroll to the top of the page first
execute_script "window.scrollTo(0,0)" execute_script "window.scrollTo(0,0)"
......
...@@ -294,7 +294,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -294,7 +294,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end end
end end
describe 'Raw trace' do describe 'Raw trace', :js do
before do before do
job.run! job.run!
...@@ -302,7 +302,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -302,7 +302,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end end
it do it do
expect(page).to have_css('.js-raw-link') wait_for_all_requests
expect(page).to have_css('.js-raw-link-controller')
end end
end end
...@@ -636,7 +637,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -636,7 +637,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end end
end end
context 'Canceled job' do context 'Canceled job', :js do
context 'with log' do context 'with log' do
let(:job) { create(:ci_build, :canceled, :trace_artifact, pipeline: pipeline) } let(:job) { create(:ci_build, :canceled, :trace_artifact, pipeline: pipeline) }
...@@ -645,7 +646,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -645,7 +646,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end end
it 'renders job log' do it 'renders job log' do
expect(page).to have_selector('.js-build-output') wait_for_all_requests
expect(page).to have_selector('.js-build-trace')
end end
end end
...@@ -658,7 +660,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -658,7 +660,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders empty state' do it 'renders empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title]) expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).not_to have_selector('.js-build-output') expect(page).not_to have_selector('.js-build-trace')
expect(page).to have_content('This job has been canceled') expect(page).to have_content('This job has been canceled')
end end
end end
...@@ -673,7 +675,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -673,7 +675,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders empty state' do it 'renders empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title]) expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).not_to have_selector('.js-build-output') expect(page).not_to have_selector('.js-build-trace')
expect(page).to have_content('This job has been skipped') expect(page).to have_content('This job has been skipped')
end end
end end
...@@ -722,8 +724,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -722,8 +724,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
visit project_job_path(project, job) visit project_job_path(project, job)
wait_for_requests wait_for_requests
expect(page).to have_css('.js-build-sidebar.right-sidebar-collapsed', visible: false) expect(page).to have_css('.js-job-sidebar.right-sidebar-collapsed', visible: false)
expect(page).not_to have_css('.js-build-sidebar.right-sidebar-expanded', visible: false) expect(page).not_to have_css('.js-job-sidebar.right-sidebar-expanded', visible: false)
end end
end end
...@@ -734,8 +736,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -734,8 +736,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
visit project_job_path(project, job) visit project_job_path(project, job)
wait_for_requests wait_for_requests
expect(page).to have_css('.js-build-sidebar.right-sidebar-expanded') expect(page).to have_css('.js-job-sidebar.right-sidebar-expanded')
expect(page).not_to have_css('.js-build-sidebar.right-sidebar-collpased') expect(page).not_to have_css('.js-job-sidebar.right-sidebar-collpased')
end end
end end
end end
......
import $ from 'jquery'; // import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter'; // import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; // import axios from '~/lib/utils/axios_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils'; // import { numberToHumanSize } from '~/lib/utils/number_utils';
import '~/lib/utils/datetime_utility'; // import '~/lib/utils/datetime_utility';
import Job from '~/job'; // import Job from '~/job';
import '~/breakpoints'; // import '~/breakpoints';
import waitForPromises from 'spec/helpers/wait_for_promises'; // import waitForPromises from 'spec/helpers/wait_for_promises';
describe('Job', () => { // describe('Job', () => {
const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; // const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
let mock; // let mock;
let response; // let response;
let job; // let job;
preloadFixtures('builds/build-with-artifacts.html.raw'); // preloadFixtures('builds/build-with-artifacts.html.raw');
beforeEach(() => { // beforeEach(() => {
loadFixtures('builds/build-with-artifacts.html.raw'); // loadFixtures('builds/build-with-artifacts.html.raw');
spyOnDependency(Job, 'visitUrl'); // spyOnDependency(Job, 'visitUrl');
response = {}; // response = {};
mock = new MockAdapter(axios); // mock = new MockAdapter(axios);
mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]); // mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]);
}); // });
afterEach(() => { // afterEach(() => {
mock.restore(); // mock.restore();
clearTimeout(job.timeout); // clearTimeout(job.timeout);
}); // });
describe('class constructor', () => { // describe('class constructor', () => {
beforeEach(() => { // beforeEach(() => {
jasmine.clock().install(); // jasmine.clock().install();
}); // });
afterEach(() => { // afterEach(() => {
jasmine.clock().uninstall(); // jasmine.clock().uninstall();
}); // });
describe('setup', () => { // describe('running build', () => {
beforeEach(function (done) { // it('updates the build trace on an interval', function (done) {
job = new Job(); // response = {
// html: '<span>Update<span>',
waitForPromises() // status: 'running',
.then(done) // state: 'newstate',
.catch(done.fail); // append: true,
}); // complete: false,
// };
it('copies build options', function () {
expect(job.pagePath).toBe(JOB_URL); // job = new Job();
expect(job.buildStatus).toBe('success');
expect(job.buildStage).toBe('test'); // waitForPromises()
expect(job.state).toBe(''); // .then(() => {
}); // expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
}); // expect(job.state).toBe('newstate');
describe('running build', () => { // response = {
it('updates the build trace on an interval', function (done) { // html: '<span>More</span>',
response = { // status: 'running',
html: '<span>Update<span>', // state: 'finalstate',
status: 'running', // append: true,
state: 'newstate', // complete: true,
append: true, // };
complete: false, // })
}; // .then(() => jasmine.clock().tick(4001))
// .then(waitForPromises)
job = new Job(); // .then(() => {
// expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
waitForPromises() // expect(job.state).toBe('finalstate');
.then(() => { // })
expect($('#build-trace .js-build-output').text()).toMatch(/Update/); // .then(done)
expect(job.state).toBe('newstate'); // .catch(done.fail);
// });
response = {
html: '<span>More</span>', // it('replaces the entire build trace', (done) => {
status: 'running', // response = {
state: 'finalstate', // html: '<span>Update<span>',
append: true, // status: 'running',
complete: true, // append: false,
}; // complete: false,
}) // };
.then(() => jasmine.clock().tick(4001))
.then(waitForPromises) // job = new Job();
.then(() => {
expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); // waitForPromises()
expect(job.state).toBe('finalstate'); // .then(() => {
}) // expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
.then(done)
.catch(done.fail); // response = {
}); // html: '<span>Different</span>',
// status: 'running',
it('replaces the entire build trace', (done) => { // append: false,
response = { // };
html: '<span>Update<span>', // })
status: 'running', // .then(() => jasmine.clock().tick(4001))
append: false, // .then(waitForPromises)
complete: false, // .then(() => {
}; // expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
// expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
job = new Job(); // })
// .then(done)
waitForPromises() // .catch(done.fail);
.then(() => { // });
expect($('#build-trace .js-build-output').text()).toMatch(/Update/); // });
response = { // describe('truncated information', () => {
html: '<span>Different</span>', // describe('when size is less than total', () => {
status: 'running', // it('shows information about truncated log', (done) => {
append: false, // response = {
}; // html: '<span>Update</span>',
}) // status: 'success',
.then(() => jasmine.clock().tick(4001)) // append: false,
.then(waitForPromises) // size: 50,
.then(() => { // total: 100,
expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); // };
expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
}) // job = new Job();
.then(done)
.catch(done.fail); // waitForPromises()
}); // .then(() => {
}); // expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
// })
describe('truncated information', () => { // .then(done)
describe('when size is less than total', () => { // .catch(done.fail);
it('shows information about truncated log', (done) => { // });
response = {
html: '<span>Update</span>', // it('shows the size in KiB', (done) => {
status: 'success', // const size = 50;
append: false,
size: 50, // response = {
total: 100, // html: '<span>Update</span>',
}; // status: 'success',
// append: false,
job = new Job(); // size,
// total: 100,
waitForPromises() // };
.then(() => {
expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); // job = new Job();
})
.then(done) // waitForPromises()
.catch(done.fail); // .then(() => {
}); // expect(
// document.querySelector('.js-truncated-info-size').textContent.trim(),
it('shows the size in KiB', (done) => { // ).toEqual(`${numberToHumanSize(size)}`);
const size = 50; // })
// .then(done)
response = { // .catch(done.fail);
html: '<span>Update</span>', // });
status: 'success',
append: false, // it('shows incremented size', (done) => {
size, // response = {
total: 100, // html: '<span>Update</span>',
}; // status: 'success',
// append: false,
job = new Job(); // size: 50,
// total: 100,
waitForPromises() // complete: false,
.then(() => { // };
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(), // job = new Job();
).toEqual(`${numberToHumanSize(size)}`);
}) // waitForPromises()
.then(done) // .then(() => {
.catch(done.fail); // expect(
}); // document.querySelector('.js-truncated-info-size').textContent.trim(),
// ).toEqual(`${numberToHumanSize(50)}`);
it('shows incremented size', (done) => {
response = { // response = {
html: '<span>Update</span>', // html: '<span>Update</span>',
status: 'success', // status: 'success',
append: false, // append: true,
size: 50, // size: 10,
total: 100, // total: 100,
complete: false, // complete: true,
}; // };
// })
job = new Job(); // .then(() => jasmine.clock().tick(4001))
// .then(waitForPromises)
waitForPromises() // .then(() => {
.then(() => { // expect(
expect( // document.querySelector('.js-truncated-info-size').textContent.trim(),
document.querySelector('.js-truncated-info-size').textContent.trim(), // ).toEqual(`${numberToHumanSize(60)}`);
).toEqual(`${numberToHumanSize(50)}`); // })
// .then(done)
response = { // .catch(done.fail);
html: '<span>Update</span>', // });
status: 'success',
append: true, // it('renders the raw link', () => {
size: 10, // response = {
total: 100, // html: '<span>Update</span>',
complete: true, // status: 'success',
}; // append: false,
}) // size: 50,
.then(() => jasmine.clock().tick(4001)) // total: 100,
.then(waitForPromises) // };
.then(() => {
expect( // job = new Job();
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${numberToHumanSize(60)}`); // expect(
}) // document.querySelector('.js-raw-link').textContent.trim(),
.then(done) // ).toContain('Complete Raw');
.catch(done.fail); // });
}); // });
it('renders the raw link', () => { // describe('when size is equal than total', () => {
response = { // it('does not show the trunctated information', (done) => {
html: '<span>Update</span>', // response = {
status: 'success', // html: '<span>Update</span>',
append: false, // status: 'success',
size: 50, // append: false,
total: 100, // size: 100,
}; // total: 100,
// };
job = new Job();
// job = new Job();
expect(
document.querySelector('.js-raw-link').textContent.trim(), // waitForPromises()
).toContain('Complete Raw'); // .then(() => {
}); // expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
}); // })
// .then(done)
describe('when size is equal than total', () => { // .catch(done.fail);
it('does not show the trunctated information', (done) => { // });
response = { // });
html: '<span>Update</span>', // });
status: 'success',
append: false, // describe('output trace', () => {
size: 100, // beforeEach((done) => {
total: 100, // response = {
}; // html: '<span>Update</span>',
// status: 'success',
job = new Job(); // append: false,
// size: 50,
waitForPromises() // total: 100,
.then(() => { // };
expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
}) // job = new Job();
.then(done)
.catch(done.fail); // waitForPromises()
}); // .then(done)
}); // .catch(done.fail);
}); // });
describe('output trace', () => { // it('should render trace controls', () => {
beforeEach((done) => { // const controllers = document.querySelector('.controllers');
response = {
html: '<span>Update</span>', // expect(controllers.querySelector('.js-raw-link-controller')).not.toBeNull();
status: 'success', // expect(controllers.querySelector('.js-scroll-up')).not.toBeNull();
append: false, // expect(controllers.querySelector('.js-scroll-down')).not.toBeNull();
size: 50, // });
total: 100,
}; // it('should render received output', () => {
// expect(
job = new Job(); // document.querySelector('.js-build-output').innerHTML,
// ).toEqual('<span>Update</span>');
waitForPromises() // });
.then(done) // });
.catch(done.fail); // });
});
// });
it('should render trace controls', () => {
const controllers = document.querySelector('.controllers');
expect(controllers.querySelector('.js-raw-link-controller')).not.toBeNull();
expect(controllers.querySelector('.js-scroll-up')).not.toBeNull();
expect(controllers.querySelector('.js-scroll-down')).not.toBeNull();
});
it('should render received output', () => {
expect(
document.querySelector('.js-build-output').innerHTML,
).toEqual('<span>Update</span>');
});
});
});
describe('getBuildTrace', () => {
it('should request build trace with state parameter', (done) => {
spyOn(axios, 'get').and.callThrough();
job = new Job();
setTimeout(() => {
expect(axios.get).toHaveBeenCalledWith(
`${JOB_URL}/trace.json`, { params: { state: '' } },
);
done();
}, 0);
});
});
});
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import jobApp from '~/jobs/components/job_app.vue'; import jobApp from '~/jobs/components/job_app.vue';
import createStore from '~/jobs/store'; import createStore from '~/jobs/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../store/helpers';
import job from '../mock_data';
describe('Job App ', () => { describe('Job App ', () => {
const Component = Vue.extend(jobApp); const Component = Vue.extend(jobApp);
let store; let store;
let vm; let vm;
let mock;
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
const twoDaysAgo = new Date();
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
const job = {
status: {
group: 'failed',
icon: 'status_failed',
label: 'failed',
text: 'failed',
details_path: 'path',
},
id: 123,
created_at: threeWeeksAgo.toISOString(),
user: {
web_url: 'path',
name: 'Foo',
username: 'foobar',
email: 'foo@bar.com',
avatar_url: 'link',
},
started: twoDaysAgo.toISOString(),
new_issue_path: 'path',
runners: {
available: false,
},
tags: ['docker'],
has_trace: true,
};
const props = { const props = {
endpoint: `${gl.TEST_HOST}jobs/123.json`,
runnerHelpUrl: 'help/runner',
runnerSettingsUrl: 'settings/ci-cd/runners', runnerSettingsUrl: 'settings/ci-cd/runners',
terminalPath: 'jobs/123/terminal',
pagePath: `${gl.TEST_HOST}jobs/123`,
logState:
'eyJvZmZzZXQiOjE3NDUxLCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowfQ%3D%3D',
}; };
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios);
store = createStore(); store = createStore();
}); });
afterEach(() => { afterEach(() => {
resetStore(store);
vm.$destroy(); vm.$destroy();
mock.restore();
}); });
describe('Header section', () => { describe('while loading', () => {
describe('job callout message', () => { beforeEach(() => {
it('should not render the reason when reason is absent', () => { mock.onGet(props.endpoint).reply(200, job, {});
store.dispatch('receiveJobSuccess', job); mock.onGet(`${props.pagePath}/trace.json`).reply(200, {});
vm = mountComponentWithStore(Component, { props, store });
});
it('renders loading icon', done => {
expect(vm.$el.querySelector('.js-job-loading')).not.toBeNull();
expect(vm.$el.querySelector('.js-job-sidebar')).toBeNull();
expect(vm.$el.querySelector('.js-job-content')).toBeNull();
setTimeout(() => {
done();
}, 0);
});
});
describe('with successfull request', () => {
beforeEach(() => {
mock.onGet(`${props.pagePath}/trace.json`).replyOnce(200, {});
});
describe('Header section', () => {
describe('job callout message', () => {
it('should not render the reason when reason is absent', done => {
mock.onGet(props.endpoint).replyOnce(200, job);
vm = mountComponentWithStore(Component, { props, store });
setTimeout(() => {
expect(vm.shouldRenderCalloutMessage).toBe(false);
done();
}, 0);
});
it('should render the reason when reason is present', done => {
mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
callout_message: 'There is an unknown failure, please try again',
}),
);
vm = mountComponentWithStore(Component, { props, store });
setTimeout(() => {
expect(vm.shouldRenderCalloutMessage).toBe(true);
done();
}, 0);
});
});
describe('triggered job', () => {
beforeEach(() => {
mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' }));
vm = mountComponentWithStore(Component, { props, store });
});
it('should render provided job information', done => {
setTimeout(() => {
expect(
vm.$el
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
).toEqual('passed Job #4757 triggered 1 year ago by Root');
done();
}, 0);
});
it('should render new issue link', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
job.new_issue_path,
);
done();
}, 0);
});
});
describe('created job', () => {
it('should render created key', done => {
mock.onGet(props.endpoint).replyOnce(200, job);
vm = mountComponentWithStore(Component, { props, store });
setTimeout(() => {
expect(
vm.$el
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
).toEqual('passed Job #4757 created 3 weeks ago by Root');
done();
}, 0);
});
});
});
describe('stuck block', () => {
it('renders stuck block when there are no runners', done => {
mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
status: {
group: 'pending',
icon: 'status_pending',
label: 'pending',
text: 'pending',
details_path: 'path',
},
runners: {
available: false,
},
}),
);
vm = mountComponentWithStore(Component, { props, store });
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull();
done();
}, 0);
});
it('renders tags in stuck block when there are no runners', done => {
mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
status: {
group: 'pending',
icon: 'status_pending',
label: 'pending',
text: 'pending',
details_path: 'path',
},
runners: {
available: false,
},
}),
);
vm = mountComponentWithStore(Component, { vm = mountComponentWithStore(Component, {
props, props,
store, store,
}); });
expect(vm.shouldRenderCalloutMessage).toBe(false); setTimeout(() => {
expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]);
done();
}, 0);
}); });
it('should render the reason when reason is present', () => { it('does not renders stuck block when there are no runners', done => {
store.dispatch( mock.onGet(props.endpoint).replyOnce(
'receiveJobSuccess', 200,
Object.assign({}, job, { Object.assign({}, job, {
callout_message: 'There is an unknown failure, please try again', runners: { available: true },
}), }),
); );
...@@ -78,246 +199,324 @@ describe('Job App ', () => { ...@@ -78,246 +199,324 @@ describe('Job App ', () => {
store, store,
}); });
expect(vm.shouldRenderCalloutMessage).toBe(true); setTimeout(() => {
expect(vm.$el.querySelector('.js-job-stuck')).toBeNull();
done();
}, 0);
}); });
}); });
describe('triggered job', () => { describe('environments block', () => {
beforeEach(() => { it('renders environment block when job has environment', done => {
store.dispatch('receiveJobSuccess', job); mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
deployment_status: {
environment: {
environment_path: '/path',
name: 'foo',
},
},
}),
);
vm = mountComponentWithStore(Component, { vm = mountComponentWithStore(Component, {
props, props,
store, store,
}); });
});
it('should render provided job information', () => { setTimeout(() => {
expect( expect(vm.$el.querySelector('.js-job-environment')).not.toBeNull();
vm.$el
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
).toEqual('failed Job #123 triggered 2 days ago by Foo');
});
it('should render new issue link', () => { done();
expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual( }, 0);
job.new_issue_path,
);
}); });
});
describe('created job', () => { it('does not render environment block when job has environment', done => {
it('should render created key', () => { mock.onGet(props.endpoint).replyOnce(200, job);
store.dispatch('receiveJobSuccess', Object.assign({}, job, { started: false }));
vm = mountComponentWithStore(Component, { vm = mountComponentWithStore(Component, {
props, props,
store, store,
}); });
expect( setTimeout(() => {
vm.$el expect(vm.$el.querySelector('.js-job-environment')).toBeNull();
.querySelector('.header-main-content') done();
.textContent.replace(/\s+/g, ' ') }, 0);
.trim(),
).toEqual('failed Job #123 created 3 weeks ago by Foo');
}); });
}); });
});
describe('stuck block', () => { describe('erased block', () => {
it('renders stuck block when there are no runners', () => { it('renders erased block when `erased` is true', done => {
store.dispatch( mock.onGet(props.endpoint).replyOnce(
'receiveJobSuccess', 200,
Object.assign({}, job, { Object.assign({}, job, {
status: { erased_by: {
group: 'pending', username: 'root',
icon: 'status_pending', web_url: 'gitlab.com/root',
label: 'pending', },
text: 'pending', erased_at: '2016-11-07T11:11:16.525Z',
details_path: 'path', }),
}, );
}),
);
vm = mountComponentWithStore(Component, {
props,
store,
});
expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); vm = mountComponentWithStore(Component, {
}); props,
store,
});
it('renders tags in stuck block when there are no runners', () => { setTimeout(() => {
store.dispatch( expect(vm.$el.querySelector('.js-job-erased-block')).not.toBeNull();
'receiveJobSuccess',
Object.assign({}, job, { done();
status: { }, 0);
group: 'pending',
icon: 'status_pending',
label: 'pending',
text: 'pending',
details_path: 'path',
},
}),
);
vm = mountComponentWithStore(Component, {
props,
store,
}); });
expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); it('does not render erased block when `erased` is false', done => {
}); mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { erased_at: null }));
it(' does not renders stuck block when there are no runners', () => { vm = mountComponentWithStore(Component, {
store.dispatch('receiveJobSuccess', Object.assign({}, job, { runners: { available: true } })); props,
store,
});
vm = mountComponentWithStore(Component, { setTimeout(() => {
props, expect(vm.$el.querySelector('.js-job-erased-block')).toBeNull();
store,
});
expect(vm.$el.querySelector('.js-job-stuck')).toBeNull(); done();
}, 0);
});
}); });
});
describe('environments block', () => { describe('empty states block', () => {
it('renders environment block when job has environment', () => { it('renders empty state when job does not have trace and is not running', done => {
store.dispatch( mock.onGet(props.endpoint).replyOnce(
'receiveJobSuccess', 200,
Object.assign({}, job, { Object.assign({}, job, {
deployment_status: { has_trace: false,
environment: { status: {
environment_path: '/path', group: 'pending',
name: 'foo', icon: 'status_pending',
label: 'pending',
text: 'pending',
details_path: 'path',
illustration: {
image: 'path',
size: '340',
title: 'Empty State',
content: 'This is an empty state',
},
action: {
button_title: 'Retry job',
method: 'post',
path: '/path',
},
}, },
}, }),
}), );
);
vm = mountComponentWithStore(Component, { vm = mountComponentWithStore(Component, {
props, props,
store, store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-empty-state')).not.toBeNull();
done();
}, 0);
}); });
expect(vm.$el.querySelector('.js-job-environment')).not.toBeNull(); it('does not render empty state when job does not have trace but it is running', done => {
}); mock.onGet(props.endpoint).replyOnce(
200,
Object.assign({}, job, {
has_trace: false,
status: {
group: 'running',
icon: 'status_running',
label: 'running',
text: 'running',
details_path: 'path',
},
}),
);
vm = mountComponentWithStore(Component, {
props,
store,
});
it('does not render environment block when job has environment', () => { setTimeout(() => {
store.dispatch('receiveJobSuccess', job); expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull();
vm = mountComponentWithStore(Component, { done();
props, }, 0);
store,
}); });
expect(vm.$el.querySelector('.js-job-environment')).toBeNull(); it('does not render empty state when job has trace but it is not running', done => {
}); mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { has_trace: true }));
});
vm = mountComponentWithStore(Component, {
props,
store,
});
describe('erased block', () => { setTimeout(() => {
it('renders erased block when `erased` is true', () => { expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull();
store.dispatch(
'receiveJobSuccess', done();
Object.assign({}, job, { }, 0);
erased_by: {
username: 'root',
web_url: 'gitlab.com/root',
},
erased_at: '2016-11-07T11:11:16.525Z',
}),
);
vm = mountComponentWithStore(Component, {
props,
store,
}); });
});
});
expect(vm.$el.querySelector('.js-job-erased-block')).not.toBeNull(); describe('trace output', () => {
beforeEach(() => {
mock.onGet(props.endpoint).reply(200, job, {});
}); });
it('does not render erased block when `erased` is false', () => { describe('with append flag', () => {
store.dispatch('receiveJobSuccess', Object.assign({}, job, { erased_at: null })); it('appends the log content to the existing one', done => {
mock.onGet(`${props.pagePath}/trace.json`).reply(200, {
html: '<span>More<span>',
status: 'running',
state: 'newstate',
append: true,
complete: true,
});
vm = mountComponentWithStore(Component, { vm = mountComponentWithStore(Component, {
props, props,
store, store,
}); });
expect(vm.$el.querySelector('.js-job-erased-block')).toBeNull(); vm.$store.state.trace = 'Update';
});
});
describe('empty states block', () => { setTimeout(() => {
it('renders empty state when job does not have trace and is not running', () => { expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain('Update');
store.dispatch(
'receiveJobSuccess',
Object.assign({}, job, {
has_trace: false,
status: {
group: 'pending',
icon: 'status_pending',
label: 'pending',
text: 'pending',
details_path: 'path',
illustration: {
image: 'path',
size: '340',
title: 'Empty State',
content: 'This is an empty state',
},
action: {
button_title: 'Retry job',
method: 'post',
path: '/path',
},
},
}),
);
vm = mountComponentWithStore(Component, { done();
props, }, 0);
store,
}); });
});
describe('without append flag', () => {
it('replaces the trace', done => {
mock.onGet(`${props.pagePath}/trace.json`).reply(200, {
html: '<span>Different<span>',
status: 'running',
append: false,
complete: true,
});
expect(vm.$el.querySelector('.js-job-empty-state')).not.toBeNull(); vm = mountComponentWithStore(Component, {
props,
store,
});
vm.$store.state.trace = 'Update';
setTimeout(() => {
expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain('Update');
expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain(
'Different',
);
done();
}, 0);
});
}); });
it('does not render empty state when job does not have trace but it is running', () => { describe('truncated information', () => {
store.dispatch( describe('when size is less than total', () => {
'receiveJobSuccess', it('shows information about truncated log', done => {
Object.assign({}, job, { mock.onGet(`${props.pagePath}/trace.json`).reply(200, {
has_trace: false, html: '<span>Update</span>',
status: { status: 'success',
group: 'running', append: false,
icon: 'status_running', size: 50,
label: 'running', total: 100,
text: 'running', complete: true,
details_path: 'path', });
},
}), vm = mountComponentWithStore(Component, {
); props,
store,
vm = mountComponentWithStore(Component, { });
props,
store, setTimeout(() => {
expect(vm.$el.querySelector('.js-truncated-info').textContent.trim()).toContain(
'50 bytes',
);
done();
}, 0);
});
}); });
expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull(); describe('when size is equal than total', () => {
it('does not show the truncated information', done => {
mock.onGet(`${props.pagePath}/trace.json`).reply(200, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 100,
total: 100,
complete: true,
});
vm = mountComponentWithStore(Component, {
props,
store,
});
setTimeout(() => {
expect(vm.$el.querySelector('.js-truncated-info').textContent.trim()).not.toContain(
'50 bytes',
);
done();
}, 0);
});
});
}); });
it('does not render empty state when job has trace but it is not running', () => { describe('trace controls', () => {
store.dispatch('receiveJobSuccess', Object.assign({}, job, { has_trace: true })); beforeEach(() => {
mock.onGet(`${props.pagePath}/trace.json`).reply(200, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
complete: true,
});
vm = mountComponentWithStore(Component, {
props,
store,
});
});
it('should render scroll buttons', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-scroll-top')).not.toBeNull();
expect(vm.$el.querySelector('.js-scroll-bottom')).not.toBeNull();
done();
}, 0);
});
vm = mountComponentWithStore(Component, { it('should render link to raw ouput', done => {
props, setTimeout(() => {
store, expect(vm.$el.querySelector('.js-raw-link-controller')).not.toBeNull();
done();
}, 0);
}); });
expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull(); it('should render link to erase job', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-erase-link')).not.toBeNull();
done();
}, 0);
});
}); });
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import component from '~/jobs/components/job_log.vue'; import component from '~/jobs/components/job_log.vue';
import mountComponent from '../../helpers/vue_mount_component_helper'; import createStore from '~/jobs/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../store/helpers';
describe('Job Log', () => { describe('Job Log', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
let store;
let vm; let vm;
const trace = 'Running with gitlab-runner 11.1.0 (081978aa)<br> on docker-auto-scale-com d5ae8d25<br>Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29 ...<br>'; const trace = 'Running with gitlab-runner 11.1.0 (081978aa)<br> on docker-auto-scale-com d5ae8d25<br>Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29 ...<br>';
beforeEach(() => {
store = createStore();
});
afterEach(() => { afterEach(() => {
resetStore(store);
vm.$destroy(); vm.$destroy();
}); });
it('renders provided trace', () => { it('renders provided trace', () => {
vm = mountComponent(Component, { vm = mountComponentWithStore(Component, {
trace, props: {
isComplete: true, trace,
isComplete: true,
},
store,
}); });
expect(vm.$el.querySelector('code').textContent).toContain('Running with gitlab-runner 11.1.0 (081978aa)'); expect(vm.$el.querySelector('code').textContent).toContain('Running with gitlab-runner 11.1.0 (081978aa)');
...@@ -23,9 +34,12 @@ describe('Job Log', () => { ...@@ -23,9 +34,12 @@ describe('Job Log', () => {
describe('while receiving trace', () => { describe('while receiving trace', () => {
it('renders animation', () => { it('renders animation', () => {
vm = mountComponent(Component, { vm = mountComponentWithStore(Component, {
trace, props: {
isComplete: true, trace,
isComplete: false,
},
store,
}); });
expect(vm.$el.querySelector('.js-log-animation')).not.toBeNull(); expect(vm.$el.querySelector('.js-log-animation')).not.toBeNull();
...@@ -34,9 +48,12 @@ describe('Job Log', () => { ...@@ -34,9 +48,12 @@ describe('Job Log', () => {
describe('when build trace has finishes', () => { describe('when build trace has finishes', () => {
it('does not render animation', () => { it('does not render animation', () => {
vm = mountComponent(Component, { vm = mountComponentWithStore(Component, {
trace, props: {
isComplete: false, trace,
isComplete: true,
},
store,
}); });
expect(vm.$el.querySelector('.js-log-animation')).toBeNull(); expect(vm.$el.querySelector('.js-log-animation')).toBeNull();
......
...@@ -18,15 +18,6 @@ describe('Sidebar details block', () => { ...@@ -18,15 +18,6 @@ describe('Sidebar details block', () => {
vm.$destroy(); vm.$destroy();
}); });
describe('when it is loading', () => {
it('should render a loading spinner', () => {
store.dispatch('requestJob');
vm = mountComponentWithStore(SidebarComponent, { store });
expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
});
});
describe('when there is no retry path retry', () => { describe('when there is no retry path retry', () => {
it('should not render a retry button', () => { it('should not render a retry button', () => {
const copy = Object.assign({}, job); const copy = Object.assign({}, job);
...@@ -52,12 +43,12 @@ describe('Sidebar details block', () => { ...@@ -52,12 +43,12 @@ describe('Sidebar details block', () => {
describe('with terminal path', () => { describe('with terminal path', () => {
it('renders terminal link', () => { it('renders terminal link', () => {
store.dispatch('receiveJobSuccess', job); store.dispatch(
'receiveJobSuccess',
Object.assign({}, job, { terminal_path: 'job/43123/terminal' }),
);
vm = mountComponentWithStore(SidebarComponent, { vm = mountComponentWithStore(SidebarComponent, {
store, store,
props: {
terminalPath: 'job/43123/terminal',
},
}); });
expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull(); expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull();
......
...@@ -31,6 +31,7 @@ export default { ...@@ -31,6 +31,7 @@ export default {
}, },
coverage: 20, coverage: 20,
erased_at: threeWeeksAgo.toISOString(), erased_at: threeWeeksAgo.toISOString(),
erased: false,
duration: 6.785563, duration: 6.785563,
tags: ['tag'], tags: ['tag'],
user: { user: {
...@@ -131,6 +132,7 @@ export default { ...@@ -131,6 +132,7 @@ export default {
path: '/root/ci-mock/merge_requests/2', path: '/root/ci-mock/merge_requests/2',
}, },
raw_path: '/root/ci-mock/builds/4757/raw', raw_path: '/root/ci-mock/builds/4757/raw',
has_trace: true,
}; };
export const stages = [ export const stages = [
......
...@@ -2,9 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -2,9 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { import {
setJobEndpoint, setJobEndpoint,
setTraceEndpoint, setTraceOptions,
setStagesEndpoint,
setJobsEndpoint,
clearEtagPoll, clearEtagPoll,
stopPolling, stopPolling,
requestJob, requestJob,
...@@ -18,10 +16,6 @@ import { ...@@ -18,10 +16,6 @@ import {
stopPollingTrace, stopPollingTrace,
receiveTraceSuccess, receiveTraceSuccess,
receiveTraceError, receiveTraceError,
fetchFavicon,
requestStatusFavicon,
receiveStatusFaviconSuccess,
requestStatusFaviconError,
requestStages, requestStages,
fetchStages, fetchStages,
receiveStagesSuccess, receiveStagesSuccess,
...@@ -30,6 +24,9 @@ import { ...@@ -30,6 +24,9 @@ import {
fetchJobsForStage, fetchJobsForStage,
receiveJobsForStageSuccess, receiveJobsForStageSuccess,
receiveJobsForStageError, receiveJobsForStageError,
hideSidebar,
showSidebar,
toggleSidebar,
} from '~/jobs/store/actions'; } from '~/jobs/store/actions';
import state from '~/jobs/store/state'; import state from '~/jobs/store/state';
import * as types from '~/jobs/store/mutation_types'; import * as types from '~/jobs/store/mutation_types';
...@@ -56,45 +53,75 @@ describe('Job State actions', () => { ...@@ -56,45 +53,75 @@ describe('Job State actions', () => {
}); });
}); });
describe('setTraceEndpoint', () => { describe('setTraceOptions', () => {
it('should commit SET_TRACE_ENDPOINT mutation', done => { it('should commit SET_TRACE_OPTIONS mutation', done => {
testAction( testAction(
setTraceEndpoint, setTraceOptions,
'job/872324/trace.json', { pagePath: 'job/872324/trace.json' },
mockedState, mockedState,
[{ type: types.SET_TRACE_ENDPOINT, payload: 'job/872324/trace.json' }], [{ type: types.SET_TRACE_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }],
[], [],
done, done,
); );
}); });
}); });
describe('setStagesEndpoint', () => { describe('hideSidebar', () => {
it('should commit SET_STAGES_ENDPOINT mutation', done => { it('should commit HIDE_SIDEBAR mutation', done => {
testAction( testAction(
setStagesEndpoint, hideSidebar,
'job/872324/stages.json', null,
mockedState, mockedState,
[{ type: types.SET_STAGES_ENDPOINT, payload: 'job/872324/stages.json' }], [{ type: types.HIDE_SIDEBAR }],
[], [],
done, done,
); );
}); });
}); });
describe('setJobsEndpoint', () => { describe('showSidebar', () => {
it('should commit SET_JOBS_ENDPOINT mutation', done => { it('should commit HIDE_SIDEBAR mutation', done => {
testAction( testAction(
setJobsEndpoint, showSidebar,
'job/872324/stages/build.json', null,
mockedState, mockedState,
[{ type: types.SET_JOBS_ENDPOINT, payload: 'job/872324/stages/build.json' }], [{ type: types.SHOW_SIDEBAR }],
[], [],
done, done,
); );
}); });
}); });
describe('toggleSidebar', () => {
describe('when isSidebarOpen is true', () => {
it('should dispatch hideSidebar', done => {
testAction(
toggleSidebar,
null,
mockedState,
[],
[{ type: 'hideSidebar' }],
done,
);
});
});
describe('when isSidebarOpen is false', () => {
it('should dispatch showSidebar', done => {
mockedState.isSidebarOpen = false;
testAction(
toggleSidebar,
null,
mockedState,
[],
[{ type: 'showSidebar' }],
done,
);
});
});
});
describe('requestJob', () => { describe('requestJob', () => {
it('should commit REQUEST_JOB mutation', done => { it('should commit REQUEST_JOB mutation', done => {
testAction(requestJob, null, mockedState, [{ type: types.REQUEST_JOB }], [], done); testAction(requestJob, null, mockedState, [{ type: types.REQUEST_JOB }], [], done);
...@@ -183,14 +210,14 @@ describe('Job State actions', () => { ...@@ -183,14 +210,14 @@ describe('Job State actions', () => {
}); });
describe('scrollTop', () => { describe('scrollTop', () => {
it('should commit SCROLL_TO_TOP mutation', done => { it('should dispatch toggleScrollButtons action', done => {
testAction(scrollTop, null, mockedState, [{ type: types.SCROLL_TO_TOP }], [], done); testAction(scrollTop, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
}); });
}); });
describe('scrollBottom', () => { describe('scrollBottom', () => {
it('should commit SCROLL_TO_BOTTOM mutation', done => { it('should dispatch toggleScrollButtons action', done => {
testAction(scrollBottom, null, mockedState, [{ type: types.SCROLL_TO_BOTTOM }], [], done); testAction(scrollBottom, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
}); });
}); });
...@@ -215,7 +242,7 @@ describe('Job State actions', () => { ...@@ -215,7 +242,7 @@ describe('Job State actions', () => {
}); });
describe('success', () => { describe('success', () => {
it('dispatches requestTrace, fetchFavicon, receiveTraceSuccess and stopPollingTrace when job is complete', done => { it('dispatches requestTrace, receiveTraceSuccess and stopPollingTrace when job is complete', done => {
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, { mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :', html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: true, complete: true,
...@@ -228,10 +255,8 @@ describe('Job State actions', () => { ...@@ -228,10 +255,8 @@ describe('Job State actions', () => {
[], [],
[ [
{ {
type: 'requestTrace', type: 'toggleScrollisInBottom',
}, payload: true,
{
type: 'fetchFavicon',
}, },
{ {
payload: { payload: {
...@@ -261,9 +286,6 @@ describe('Job State actions', () => { ...@@ -261,9 +286,6 @@ describe('Job State actions', () => {
mockedState, mockedState,
[], [],
[ [
{
type: 'requestTrace',
},
{ {
type: 'receiveTraceError', type: 'receiveTraceError',
}, },
...@@ -313,104 +335,6 @@ describe('Job State actions', () => { ...@@ -313,104 +335,6 @@ describe('Job State actions', () => {
}); });
}); });
describe('fetchFavicon', () => {
let mock;
beforeEach(() => {
mockedState.pagePath = `${TEST_HOST}/endpoint`;
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('success', () => {
it('dispatches requestStatusFavicon and receiveStatusFaviconSuccess ', done => {
mock.onGet(`${TEST_HOST}/endpoint/status.json`).replyOnce(200);
testAction(
fetchFavicon,
null,
mockedState,
[],
[
{
type: 'requestStatusFavicon',
},
{
type: 'receiveStatusFaviconSuccess',
},
],
done,
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/endpoint/status.json`).replyOnce(500);
});
it('dispatches requestStatusFavicon and requestStatusFaviconError ', done => {
testAction(
fetchFavicon,
null,
mockedState,
[],
[
{
type: 'requestStatusFavicon',
},
{
type: 'requestStatusFaviconError',
},
],
done,
);
});
});
});
describe('requestStatusFavicon', () => {
it('should commit REQUEST_STATUS_FAVICON mutation ', done => {
testAction(
requestStatusFavicon,
null,
mockedState,
[{ type: types.REQUEST_STATUS_FAVICON }],
[],
done,
);
});
});
describe('receiveStatusFaviconSuccess', () => {
it('should commit RECEIVE_STATUS_FAVICON_SUCCESS mutation ', done => {
testAction(
receiveStatusFaviconSuccess,
null,
mockedState,
[{ type: types.RECEIVE_STATUS_FAVICON_SUCCESS }],
[],
done,
);
});
});
describe('requestStatusFaviconError', () => {
it('should commit RECEIVE_STATUS_FAVICON_ERROR mutation ', done => {
testAction(
requestStatusFaviconError,
null,
mockedState,
[{ type: types.RECEIVE_STATUS_FAVICON_ERROR }],
[],
done,
);
});
});
describe('requestStages', () => { describe('requestStages', () => {
it('should commit REQUEST_STAGES mutation ', done => { it('should commit REQUEST_STAGES mutation ', done => {
testAction(requestStages, null, mockedState, [{ type: types.REQUEST_STAGES }], [], done); testAction(requestStages, null, mockedState, [{ type: types.REQUEST_STAGES }], [], done);
......
import state from '~/jobs/store/state';
// eslint-disable-next-line import/prefer-default-export
export const resetStore = store => {
store.replaceState(state());
};
...@@ -20,27 +20,19 @@ describe('Jobs Store Mutations', () => { ...@@ -20,27 +20,19 @@ describe('Jobs Store Mutations', () => {
}); });
}); });
describe('REQUEST_STATUS_FAVICON', () => { describe('HIDE_SIDEBAR', () => {
it('should set fetchingStatusFavicon to true', () => { it('should set isSidebarOpen to false', () => {
mutations[types.REQUEST_STATUS_FAVICON](stateCopy); mutations[types.HIDE_SIDEBAR](stateCopy);
expect(stateCopy.fetchingStatusFavicon).toEqual(true); expect(stateCopy.isSidebarOpen).toEqual(false);
}); });
}); });
describe('RECEIVE_STATUS_FAVICON_SUCCESS', () => { describe('SHOW_SIDEBAR', () => {
it('should set fetchingStatusFavicon to false', () => { it('should set isSidebarOpen to true', () => {
mutations[types.RECEIVE_STATUS_FAVICON_SUCCESS](stateCopy); mutations[types.SHOW_SIDEBAR](stateCopy);
expect(stateCopy.fetchingStatusFavicon).toEqual(false); expect(stateCopy.isSidebarOpen).toEqual(true);
});
});
describe('RECEIVE_STATUS_FAVICON_ERROR', () => {
it('should set fetchingStatusFavicon to false', () => {
mutations[types.RECEIVE_STATUS_FAVICON_ERROR](stateCopy);
expect(stateCopy.fetchingStatusFavicon).toEqual(false);
}); });
}); });
...@@ -101,9 +93,7 @@ describe('Jobs Store Mutations', () => { ...@@ -101,9 +93,7 @@ describe('Jobs Store Mutations', () => {
it('resets trace state and sets error to true', () => { it('resets trace state and sets error to true', () => {
mutations[types.RECEIVE_TRACE_ERROR](stateCopy); mutations[types.RECEIVE_TRACE_ERROR](stateCopy);
expect(stateCopy.isLoadingTrace).toEqual(false);
expect(stateCopy.isTraceComplete).toEqual(true); expect(stateCopy.isTraceComplete).toEqual(true);
expect(stateCopy.hasTraceError).toEqual(true);
}); });
}); });
...@@ -156,39 +146,10 @@ describe('Jobs Store Mutations', () => { ...@@ -156,39 +146,10 @@ describe('Jobs Store Mutations', () => {
mutations[types.RECEIVE_JOB_ERROR](stateCopy); mutations[types.RECEIVE_JOB_ERROR](stateCopy);
expect(stateCopy.isLoading).toEqual(false); expect(stateCopy.isLoading).toEqual(false);
expect(stateCopy.hasError).toEqual(true);
expect(stateCopy.job).toEqual({}); expect(stateCopy.job).toEqual({});
}); });
}); });
describe('SCROLL_TO_TOP', () => {
beforeEach(() => {
mutations[types.SCROLL_TO_TOP](stateCopy);
});
it('sets isTraceScrolledToBottom to false', () => {
expect(stateCopy.isTraceScrolledToBottom).toEqual(false);
});
it('sets hasBeenScrolled to true', () => {
expect(stateCopy.hasBeenScrolled).toEqual(true);
});
});
describe('SCROLL_TO_BOTTOM', () => {
beforeEach(() => {
mutations[types.SCROLL_TO_BOTTOM](stateCopy);
});
it('sets isTraceScrolledToBottom to true', () => {
expect(stateCopy.isTraceScrolledToBottom).toEqual(true);
});
it('sets hasBeenScrolled to true', () => {
expect(stateCopy.hasBeenScrolled).toEqual(true);
});
});
describe('REQUEST_STAGES', () => { describe('REQUEST_STAGES', () => {
it('sets isLoadingStages to true', () => { it('sets isLoadingStages to true', () => {
mutations[types.REQUEST_STAGES](stateCopy); mutations[types.REQUEST_STAGES](stateCopy);
......
...@@ -94,7 +94,7 @@ describe('Header CI Component', () => { ...@@ -94,7 +94,7 @@ describe('Header CI Component', () => {
}); });
it('should render sidebar toggle button', () => { it('should render sidebar toggle button', () => {
expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined(); expect(vm.$el.querySelector('.js-sidebar-build-toggle')).not.toBeNull();
}); });
}); });
......
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