summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2018-10-17 10:34:19 +0000
committerPhil Hughes <me@iamphill.com>2018-10-17 10:34:19 +0000
commit5ed91cf81bcc459ad65357c128b955e10ddce284 (patch)
tree77c4b367c9c2d1a34a6eb1dafeb1040cb97904a3
parent712f41e15cb61b8804f41afddfbe5f57106248a1 (diff)
downloadgitlab-ce-5ed91cf81bcc459ad65357c128b955e10ddce284.tar.gz
Resolve "Integrate new vue+vuex code base with new API and remove old haml code"
-rw-r--r--app/assets/javascripts/job.js188
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue273
-rw-r--r--app/assets/javascripts/jobs/components/job_log.vue54
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue8
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue335
-rw-r--r--app/assets/javascripts/jobs/index.js26
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js76
-rw-r--r--app/assets/javascripts/jobs/store/actions.js117
-rw-r--r--app/assets/javascripts/jobs/store/getters.js8
-rw-r--r--app/assets/javascripts/jobs/store/mutation_types.js19
-rw-r--r--app/assets/javascripts/jobs/store/mutations.js64
-rw-r--r--app/assets/javascripts/jobs/store/state.js29
-rw-r--r--app/assets/javascripts/jobs/svg/scroll_down.svg5
-rw-r--r--app/assets/javascripts/pages/projects/jobs/show/index.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue31
-rw-r--r--app/assets/stylesheets/pages/builds.scss2
-rw-r--r--app/views/projects/artifacts/browse.html.haml2
-rw-r--r--app/views/projects/artifacts/file.html.haml2
-rw-r--r--app/views/projects/jobs/_header.html.haml10
-rw-r--r--app/views/projects/jobs/show.html.haml51
-rw-r--r--changelogs/unreleased/50904-job-log.yml5
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb2
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb22
-rw-r--r--spec/javascripts/job_spec.js560
-rw-r--r--spec/javascripts/jobs/components/job_app_spec.js645
-rw-r--r--spec/javascripts/jobs/components/job_log_spec.js37
-rw-r--r--spec/javascripts/jobs/components/sidebar_spec.js17
-rw-r--r--spec/javascripts/jobs/mock_data.js2
-rw-r--r--spec/javascripts/jobs/store/actions_spec.js188
-rw-r--r--spec/javascripts/jobs/store/helpers.js6
-rw-r--r--spec/javascripts/jobs/store/mutations_spec.js55
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js2
34 files changed, 1448 insertions, 1400 deletions
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
deleted file mode 100644
index 80ba43dca6f..00000000000
--- a/app/assets/javascripts/job.js
+++ /dev/null
@@ -1,188 +0,0 @@
-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();
- }
-}
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index 4e8d3ad24cc..fa35b87ef2b 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -1,21 +1,32 @@
<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 Callout from '~/vue_shared/components/callout.vue';
+ import createStore from '../store';
import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_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 Sidebar from './sidebar.vue';
export default {
name: 'JobPageApp',
+ store: createStore(),
components: {
CiHeader,
Callout,
EmptyState,
EnvironmentsBlock,
ErasedBlock,
+ Log,
+ LogTopBar,
StuckBlock,
+ Sidebar,
},
props: {
runnerSettingsUrl: {
@@ -23,9 +34,43 @@
required: false,
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: {
- ...mapState(['isLoading', 'job']),
+ ...mapState([
+ 'isLoading',
+ 'job',
+ 'isSidebarOpen',
+ 'trace',
+ 'isTraceComplete',
+ 'traceSize',
+ 'isTraceSizeVisible',
+ 'isScrollBottomDisabled',
+ 'isScrollTopDisabled',
+ 'isScrolledToBottomBeforeReceivingTrace',
+ 'hasError',
+ ]),
...mapGetters([
'headerActions',
'headerTime',
@@ -35,7 +80,83 @@
'isJobStuck',
'hasTrace',
'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>
@@ -44,71 +165,107 @@
<gl-loading-icon
v-if="isLoading"
:size="2"
- class="prepend-top-20"
+ class="js-job-loading prepend-top-20"
/>
- <template v-else>
- <!-- Header Section -->
- <header>
- <div class="js-build-header build-header top-area">
- <ci-header
- :status="job.status"
- :item-id="job.id"
- :time="headerTime"
- :user="job.user"
- :actions="headerActions"
- :has-sidebar-button="true"
- :should-render-triggered-label="shouldRenderTriggeredLabel"
- :item-name="__('Job')"
+ <template v-else-if="shouldRenderContent">
+ <div class="js-job-content build-page">
+ <!-- Header Section -->
+ <header>
+ <div class="js-build-header build-header top-area">
+ <ci-header
+ :status="job.status"
+ :item-id="job.id"
+ :time="headerTime"
+ :user="job.user"
+ :actions="headerActions"
+ :has-sidebar-button="true"
+ :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>
+ <!-- EO job log -->
- <callout
- v-if="shouldRenderCalloutMessage"
- :message="job.callout_message"
+ <!--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="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 Body Section -->
+ </div>
</template>
+
+ <sidebar
+ v-if="shouldRenderContent"
+ class="js-job-sidebar"
+ :class="{
+ 'right-sidebar-expanded': isSidebarOpen,
+ 'right-sidebar-collapsed': !isSidebarOpen
+ }"
+ :runner-help-url="runnerHelpUrl"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue
index 9d78d89239a..accda5d1bd8 100644
--- a/app/assets/javascripts/jobs/components/job_log.vue
+++ b/app/assets/javascripts/jobs/components/job_log.vue
@@ -1,20 +1,48 @@
<script>
-export default {
- name: 'JobLog',
- props: {
- trace: {
- type: String,
- required: true,
+ import { mapState, mapActions } from 'vuex';
+
+ export default {
+ name: 'JobLog',
+ props: {
+ trace: {
+ type: String,
+ required: true,
+ },
+ isComplete: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['isScrolledToBottomBeforeReceivingTrace']),
+ },
+ updated() {
+ this.$nextTick(() => this.handleScrollDown());
+ },
+ mounted() {
+ this.$nextTick(() => this.handleScrollDown());
},
- isComplete: {
- type: Boolean,
- required: true,
+ methods: {
+ ...mapActions(['scrollBottom']),
+ /**
+ * 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>
<template>
- <pre class="build-trace">
+ <pre class="js-build-trace build-trace">
<code
class="bash"
v-html="trace"
@@ -22,7 +50,7 @@ export default {
</code>
<div
- v-if="isComplete"
+ v-if="!isComplete"
class="js-log-animation build-loader-animation"
>
<div class="dot"></div>
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index cc885ea8e1b..94ab1b16c84 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -4,6 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale';
+import scrollDown from '../svg/scroll_down.svg';
export default {
components: {
@@ -12,6 +13,7 @@ export default {
directives: {
tooltip,
},
+ scrollDown,
props: {
erasePath: {
type: String,
@@ -65,7 +67,7 @@ export default {
};
</script>
<template>
- <div class="top-bar affix js-top-bar">
+ <div class="top-bar affix">
<!-- truncate information -->
<div class="js-truncated-info truncated-info d-none d-sm-block float-left">
<template v-if="isTraceSizeVisible">
@@ -100,7 +102,7 @@ export default {
v-tooltip
:title="s__('Job|Erase job log')"
: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"
data-container="body"
data-method="post"
@@ -138,8 +140,8 @@ export default {
class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
:class="{ animate: isScrollingDown }"
@click="handleScrollToBottom"
+ v-html="$options.scrollDown"
>
- <icon name="scroll_down"/>
</button>
</div>
<!-- eo scroll buttons -->
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 8f3c6aced23..906769ee6a2 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -29,14 +29,9 @@ export default {
required: false,
default: '',
},
- terminalPath: {
- type: String,
- required: false,
- default: null,
- },
},
computed: {
- ...mapState(['job', 'isLoading', 'stages', 'jobs', 'selectedStage']),
+ ...mapState(['job', 'stages', 'jobs', 'selectedStage']),
coverage() {
return `${this.job.coverage}%`;
},
@@ -64,10 +59,10 @@ export default {
return '';
}
- let t = this.job.metadata.timeout_human_readable;
- if (this.job.metadata.timeout_source !== '') {
- t += ` (from ${this.job.metadata.timeout_source})`;
- }
+ let t = this.job.metadata.timeout_human_readable;
+ if (this.job.metadata.timeout_source !== '') {
+ t += ` (from ${this.job.metadata.timeout_source})`;
+ }
return t;
},
@@ -100,196 +95,190 @@ export default {
);
},
commit() {
- return this.job.pipeline.commit || {};
+ return this.job.pipeline && this.job.pipeline.commit ? this.job.pipeline.commit : {};
},
},
methods: {
- ...mapActions(['fetchJobsForStage']),
+ ...mapActions(['fetchJobsForStage', 'toggleSidebar']),
},
};
</script>
<template>
<aside
- class="js-build-sidebar right-sidebar right-sidebar-expanded build-sidebar"
+ class="right-sidebar build-sidebar"
data-offset-top="101"
data-spy="affix"
>
<div class="sidebar-container">
<div class="blocks-container">
- <template v-if="!isLoading">
- <div class="block">
- <strong class="inline prepend-top-8">
- {{ job.name }}
- </strong>
- <a
- v-if="job.retry_path"
- :class="retryButtonClass"
- :href="job.retry_path"
- data-method="post"
- 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"
+ <div class="block">
+ <strong class="inline prepend-top-8">
+ {{ job.name }}
+ </strong>
+ <a
+ v-if="job.retry_path"
+ :class="retryButtonClass"
+ :href="job.retry_path"
+ data-method="post"
+ rel="nofollow"
>
- <a
- v-if="job.new_issue_path"
- :href="job.new_issue_path"
- class="js-new-issue btn btn-success btn-inverted"
- >
- {{ __('New issue') }}
+ {{ __('Retry') }}
+ </a>
+ <a
+ v-if="job.terminal_path"
+ :href="job.terminal_path"
+ 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>
+ </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
- v-if="job.retry_path"
- :href="job.retry_path"
- class="js-retry-job btn btn-inverted-secondary"
+ :href="job.cancel_path"
+ class="js-cancel-job btn btn-sm btn-default"
data-method="post"
rel="nofollow"
>
- {{ __('Retry') }}
+ {{ __('Cancel') }}
</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>
- </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"
- />
+ </div>
- <stages-dropdown
- :stages="stages"
- :pipeline="job.pipeline"
- :selected-stage="selectedStage"
- @requestSidebarStageDropdown="fetchJobsForStage"
- />
+ <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"
+ />
- </template>
- <gl-loading-icon
- v-else
- :size="2"
- class="prepend-top-10"
+ <stages-dropdown
+ :stages="stages"
+ :pipeline="job.pipeline"
+ :selected-stage="selectedStage"
+ @requestSidebarStageDropdown="fetchJobsForStage"
/>
</div>
<jobs-container
- v-if="!isLoading && jobs.length"
+ v-if="jobs.length"
:jobs="jobs"
:job-id="job.id"
/>
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
new file mode 100644
index 00000000000..ccd096a1da5
--- /dev/null
+++ b/app/assets/javascripts/jobs/index.js
@@ -0,0 +1,26 @@
+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,
+ },
+ });
+ },
+ });
+};
+
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
deleted file mode 100644
index 15cd79b1c50..00000000000
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ /dev/null
@@ -1,76 +0,0 @@
-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();
-};
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index d0040161dc3..54ed217572a 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -1,17 +1,32 @@
import Visibility from 'visibilityjs';
import * as types from './mutation_types';
-import axios from '../../lib/utils/axios_utils';
-import Poll from '../../lib/utils/poll';
-import { setCiStatusFavicon } from '../../lib/utils/common_utils';
-import flash from '../../flash';
-import { __ } from '../../locale';
+import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
+import { setFaviconOverlay, resetFavicon } from '~/lib/utils/common_utils';
+import flash from '~/flash';
+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 setTraceEndpoint = ({ commit }, endpoint) =>
- commit(types.SET_TRACE_ENDPOINT, endpoint);
-export const setStagesEndpoint = ({ commit }, endpoint) =>
- commit(types.SET_STAGES_ENDPOINT, endpoint);
-export const setJobsEndpoint = ({ commit }, endpoint) => commit(types.SET_JOBS_ENDPOINT, endpoint);
+export const setTraceOptions = ({ commit }, options) => commit(types.SET_TRACE_OPTIONS, options);
+
+export const hideSidebar = ({ commit }) => commit(types.HIDE_SIDEBAR);
+export const showSidebar = ({ commit }) => commit(types.SHOW_SIDEBAR);
+
+export const toggleSidebar = ({ dispatch, state }) => {
+ if (state.isSidebarOpen) {
+ dispatch('hideSidebar');
+ } else {
+ dispatch('showSidebar');
+ }
+};
let eTagPoll;
@@ -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);
+
+ if (data.favicon) {
+ setFaviconOverlay(data.favicon);
+ } else {
+ resetFavicon();
+ }
};
export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR);
flash(__('An error occurred while fetching the job.'));
+ resetFavicon();
};
/**
* Job's Trace
*/
-export const scrollTop = ({ commit }) => {
- commit(types.SCROLL_TO_TOP);
- window.scrollTo({ top: 0 });
+export const scrollTop = ({ dispatch }) => {
+ scrollUp();
+ dispatch('toggleScrollButtons');
};
-export const scrollBottom = ({ commit }) => {
- commit(types.SCROLL_TO_BOTTOM);
- window.scrollTo({ top: document.height });
+export const scrollBottom = ({ dispatch }) => {
+ scrollDown();
+ 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);
let traceTimeout;
-export const fetchTrace = ({ dispatch, state }) => {
- dispatch('requestTrace');
-
+export const fetchTrace = ({ dispatch, state }) =>
axios
.get(`${state.traceEndpoint}/trace.json`, {
params: { state: state.traceState },
})
.then(({ data }) => {
- if (!state.fetchingStatusFavicon) {
- dispatch('fetchFavicon');
- }
+ dispatch('toggleScrollisInBottom', isScrolledToBottom());
dispatch('receiveTraceSuccess', data);
if (!data.complete) {
@@ -108,7 +166,7 @@ export const fetchTrace = ({ dispatch, state }) => {
}
})
.catch(() => dispatch('receiveTraceError'));
-};
+
export const stopPollingTrace = ({ commit }) => {
commit(types.STOP_POLLING_TRACE);
clearTimeout(traceTimeout);
@@ -120,17 +178,6 @@ export const receiveTraceError = ({ commit }) => {
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
*/
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 9f4f372e3d2..4ce395a9106 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -1,5 +1,6 @@
import _ from 'underscore';
import { __ } from '~/locale';
+import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
export const headerActions = state => {
if (state.job.new_issue_path) {
@@ -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
* @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 =>
(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
* we need to render the stuck block;
@@ -46,8 +48,10 @@ export const emptyStateIllustration = state =>
* @returns {Boolean}
*/
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);
+export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/jobs/store/mutation_types.js
index e66e1d4f116..fd098f13e90 100644
--- a/app/assets/javascripts/jobs/store/mutation_types.js
+++ b/app/assets/javascripts/jobs/store/mutation_types.js
@@ -1,10 +1,19 @@
export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT';
-export const SET_TRACE_ENDPOINT = 'SET_TRACE_ENDPOINT';
-export const SET_STAGES_ENDPOINT = 'SET_STAGES_ENDPOINT';
-export const SET_JOBS_ENDPOINT = 'SET_JOBS_ENDPOINT';
+export const SET_TRACE_OPTIONS = 'SET_TRACE_OPTIONS';
+
+export const HIDE_SIDEBAR = 'HIDE_SIDEBAR';
+export const SHOW_SIDEBAR = 'SHOW_SIDEBAR';
export const SCROLL_TO_TOP = 'SCROLL_TO_TOP';
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 RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS';
@@ -15,10 +24,6 @@ export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE';
export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS';
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 RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS';
export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR';
diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js
index f00e06e1a6c..4195d787f12 100644
--- a/app/assets/javascripts/jobs/store/mutations.js
+++ b/app/assets/javascripts/jobs/store/mutations.js
@@ -4,14 +4,17 @@ export default {
[types.SET_JOB_ENDPOINT](state, 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) {
- state.fetchingStatusFavicon = false;
+ [types.SHOW_SIDEBAR](state) {
+ state.isSidebarOpen = true;
},
[types.RECEIVE_TRACE_SUCCESS](state, log) {
@@ -23,8 +26,12 @@ export default {
state.trace += log.html;
state.traceSize += log.size;
} else {
- state.trace = log.html;
- state.traceSize = log.size;
+ // When the job still does not have a trace
+ // 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) {
@@ -33,25 +40,29 @@ export default {
state.isTraceSizeVisible = false;
}
- state.isTraceComplete = log.complete;
- state.hasTraceError = false;
+ state.isTraceComplete = log.complete || state.isTraceComplete;
},
+
+ /**
+ * Will remove loading animation
+ */
[types.STOP_POLLING_TRACE](state) {
state.isTraceComplete = true;
},
- // todo_fl: check this.
+
+ /**
+ * Will remove loading animation
+ */
[types.RECEIVE_TRACE_ERROR](state) {
- state.isLoadingTrace = false;
state.isTraceComplete = true;
- state.hasTraceError = true;
},
[types.REQUEST_JOB](state) {
state.isLoading = true;
},
[types.RECEIVE_JOB_SUCCESS](state, job) {
- state.isLoading = false;
state.hasError = false;
+ state.isLoading = false;
state.job = job;
/**
@@ -66,17 +77,28 @@ export default {
},
[types.RECEIVE_JOB_ERROR](state) {
state.isLoading = false;
- state.hasError = true;
state.job = {};
+ state.hasError = true;
},
- [types.SCROLL_TO_TOP](state) {
- state.isTraceScrolledToBottom = false;
- state.hasBeenScrolled = true;
+ [types.ENABLE_SCROLL_TOP](state) {
+ state.isScrollTopDisabled = false;
+ },
+ [types.DISABLE_SCROLL_TOP](state) {
+ state.isScrollTopDisabled = true;
+ },
+ [types.ENABLE_SCROLL_BOTTOM](state) {
+ state.isScrollBottomDisabled = false;
},
- [types.SCROLL_TO_BOTTOM](state) {
- state.isTraceScrolledToBottom = true;
- state.hasBeenScrolled = true;
+ [types.DISABLE_SCROLL_BOTTOM](state) {
+ state.isScrollBottomDisabled = 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) {
diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js
index afbc959bb71..0eb269ca38f 100644
--- a/app/assets/javascripts/jobs/store/state.js
+++ b/app/assets/javascripts/jobs/store/state.js
@@ -4,36 +4,29 @@ export default () => ({
jobEndpoint: null,
traceEndpoint: null,
- // dropdown options
- stagesEndpoint: null,
- // list of jobs on sidebard
- stageJobsEndpoint: null,
+ // sidebar
+ isSidebarOpen: true,
- // job log
isLoading: false,
hasError: false,
job: {},
- // trace
- isLoadingTrace: false,
- hasTraceError: false,
+ // scroll buttons state
+ isScrollBottomDisabled: true,
+ isScrollTopDisabled: true,
- trace: '',
-
- isTraceScrolledToBottom: false,
- hasBeenScrolled: false,
+ // Used to check if we should keep the automatic scroll
+ isScrolledToBottomBeforeReceivingTrace: true,
+ trace: '',
isTraceComplete: false,
- traceSize: 0, // todo_fl: needs to be converted into human readable format in components
+ traceSize: 0,
isTraceSizeVisible: false,
- fetchingStatusFavicon: false,
- // used as a query parameter
+ // used as a query parameter to fetch the trace
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,
isLoadingJobs: false,
selectedStage: __('More'),
diff --git a/app/assets/javascripts/jobs/svg/scroll_down.svg b/app/assets/javascripts/jobs/svg/scroll_down.svg
new file mode 100644
index 00000000000..1d22870ec09
--- /dev/null
+++ b/app/assets/javascripts/jobs/svg/scroll_down.svg
@@ -0,0 +1,5 @@
+<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>
diff --git a/app/assets/javascripts/pages/projects/jobs/show/index.js b/app/assets/javascripts/pages/projects/jobs/show/index.js
index 3626f3ffec6..d57dbeb1242 100644
--- a/app/assets/javascripts/pages/projects/jobs/show/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/show/index.js
@@ -1,3 +1,3 @@
-import initJobDetails from '~/jobs/job_details_bundle';
+import initJobDetails from '~/jobs';
document.addEventListener('DOMContentLoaded', initJobDetails);
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index b371b6adf7e..aee88cae48d 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -69,6 +69,9 @@ export default {
onClickAction(action) {
this.$emit('actionClicked', action);
},
+ onClickSidebarButton() {
+ this.$emit('clickedSidebarButton');
+ },
},
};
</script>
@@ -161,21 +164,21 @@ export default {
</i>
</button>
</template>
- <button
- v-if="hasSidebarButton"
- id="toggleSidebar"
- type="button"
- class="btn btn-default d-block d-sm-none d-md-none
+ </section>
+ <button
+ v-if="hasSidebarButton"
+ id="toggleSidebar"
+ type="button"
+ class="btn btn-default d-block d-sm-none
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
- class="fa fa-angle-double-left"
- aria-hidden="true"
- aria-labelledby="toggleSidebar"
- >
- </i>
- </button>
- </section>
+ </i>
+ </button>
</header>
</template>
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index ed877f625b5..227f49ec595 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -117,7 +117,6 @@
.controllers {
display: flex;
- font-size: 15px;
justify-content: center;
align-items: center;
@@ -179,6 +178,7 @@
.build-loader-animation {
@include build-loader-animation;
+ float: left;
}
}
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index 87b165e581a..09295940529 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title _('Artifacts')
- 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("##{@build.id}", project_jobs_path(@project))
diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml
index f7174d6b2c6..808b4acc8f3 100644
--- a/app/views/projects/artifacts/file.html.haml
+++ b/app/views/projects/artifacts/file.html.haml
@@ -1,6 +1,6 @@
- page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
-= render "projects/jobs/header", show_controls: false
+= render "projects/jobs/header"
.tree-holder
.nav-block
diff --git a/app/views/projects/jobs/_header.html.haml b/app/views/projects/jobs/_header.html.haml
index e7245622b80..018ff093475 100644
--- a/app/views/projects/jobs/_header.html.haml
+++ b/app/views/projects/jobs/_header.html.haml
@@ -1,4 +1,3 @@
-- show_controls = local_assigns.fetch(:show_controls, true)
- pipeline = @build.pipeline
.content-block.build-header.top-area.page-content-header
@@ -20,12 +19,3 @@
= render "projects/jobs/user" if @build.user
= 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')
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 02a088d338b..475bae887ec 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -1,52 +1,13 @@
- @no_container = true
-- add_to_breadcrumbs "Jobs", project_jobs_path(@project)
+- add_to_breadcrumbs _("Jobs"), project_jobs_path(@project)
- breadcrumb_title "##{@build.id}"
-- page_title "#{@build.name} (##{@build.id})", "Jobs"
+- page_title "#{@build.name} (##{@build.id})", _("Jobs")
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/xterm'
%div{ class: container_class }
- .build-page.js-build-page
- #js-build-header-vue
-
- - if @build.running? || @build.has_trace?
- .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') } }
+ #js-job-vue-app{ 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'),
+ build_options: javascript_build_options } }
diff --git a/changelogs/unreleased/50904-job-log.yml b/changelogs/unreleased/50904-job-log.yml
new file mode 100644
index 00000000000..31eb2d56b77
--- /dev/null
+++ b/changelogs/unreleased/50904-job-log.yml
@@ -0,0 +1,5 @@
+---
+title: Transform job page into a single Vue+Vuex application
+merge_request:
+author:
+type: other
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b8c4eddbec1..e7aa7ee1347 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -627,6 +627,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
+msgid "Are you sure you want to erase this build?"
+msgstr ""
+
msgid "Are you sure you want to lose unsaved changes?"
msgstr ""
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index e639f0cf82e..6ce37297a7e 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -67,7 +67,7 @@ describe 'Project Jobs Permissions' do
it_behaves_like 'recent job page details responds with status', 200 do
it 'renders job details', :js do
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
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index fc7b78ac21f..908c616f2fc 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -20,7 +20,7 @@ describe 'User browses a job', :js do
wait_for_requests
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
execute_script "window.scrollTo(0,0)"
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index a3a301504ff..7f5c3ff6ed8 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -294,7 +294,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
end
- describe 'Raw trace' do
+ describe 'Raw trace', :js do
before do
job.run!
@@ -302,7 +302,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
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
@@ -636,7 +637,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
end
- context 'Canceled job' do
+ context 'Canceled job', :js do
context 'with log' do
let(:job) { create(:ci_build, :canceled, :trace_artifact, pipeline: pipeline) }
@@ -645,7 +646,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
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
@@ -658,7 +660,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders empty state' do
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')
end
end
@@ -673,7 +675,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders empty state' do
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')
end
end
@@ -722,8 +724,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
visit project_job_path(project, job)
wait_for_requests
- expect(page).to have_css('.js-build-sidebar.right-sidebar-collapsed', visible: false)
- expect(page).not_to have_css('.js-build-sidebar.right-sidebar-expanded', visible: false)
+ expect(page).to have_css('.js-job-sidebar.right-sidebar-collapsed', visible: false)
+ expect(page).not_to have_css('.js-job-sidebar.right-sidebar-expanded', visible: false)
end
end
@@ -734,8 +736,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
visit project_job_path(project, job)
wait_for_requests
- expect(page).to have_css('.js-build-sidebar.right-sidebar-expanded')
- expect(page).not_to have_css('.js-build-sidebar.right-sidebar-collpased')
+ expect(page).to have_css('.js-job-sidebar.right-sidebar-expanded')
+ expect(page).not_to have_css('.js-job-sidebar.right-sidebar-collpased')
end
end
end
diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js
index d6b5dec9e47..bc973407b25 100644
--- a/spec/javascripts/job_spec.js
+++ b/spec/javascripts/job_spec.js
@@ -1,295 +1,265 @@
-import $ from 'jquery';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import '~/lib/utils/datetime_utility';
-import Job from '~/job';
-import '~/breakpoints';
-import waitForPromises from 'spec/helpers/wait_for_promises';
-
-describe('Job', () => {
- const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
- let mock;
- let response;
- let job;
-
- preloadFixtures('builds/build-with-artifacts.html.raw');
-
- beforeEach(() => {
- loadFixtures('builds/build-with-artifacts.html.raw');
-
- spyOnDependency(Job, 'visitUrl');
-
- response = {};
-
- mock = new MockAdapter(axios);
-
- mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]);
- });
-
- afterEach(() => {
- mock.restore();
-
- clearTimeout(job.timeout);
- });
-
- describe('class constructor', () => {
- beforeEach(() => {
- jasmine.clock().install();
- });
-
- afterEach(() => {
- jasmine.clock().uninstall();
- });
-
- describe('setup', () => {
- beforeEach(function (done) {
- job = new Job();
-
- waitForPromises()
- .then(done)
- .catch(done.fail);
- });
-
- it('copies build options', function () {
- expect(job.pagePath).toBe(JOB_URL);
- expect(job.buildStatus).toBe('success');
- expect(job.buildStage).toBe('test');
- expect(job.state).toBe('');
- });
- });
-
- describe('running build', () => {
- it('updates the build trace on an interval', function (done) {
- response = {
- html: '<span>Update<span>',
- status: 'running',
- state: 'newstate',
- append: true,
- complete: false,
- };
-
- job = new Job();
-
- waitForPromises()
- .then(() => {
- expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
- expect(job.state).toBe('newstate');
-
- response = {
- html: '<span>More</span>',
- status: 'running',
- state: 'finalstate',
- append: true,
- complete: true,
- };
- })
- .then(() => jasmine.clock().tick(4001))
- .then(waitForPromises)
- .then(() => {
- expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
- expect(job.state).toBe('finalstate');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('replaces the entire build trace', (done) => {
- response = {
- html: '<span>Update<span>',
- status: 'running',
- append: false,
- complete: false,
- };
-
- job = new Job();
-
- waitForPromises()
- .then(() => {
- expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
-
- response = {
- html: '<span>Different</span>',
- status: 'running',
- append: false,
- };
- })
- .then(() => jasmine.clock().tick(4001))
- .then(waitForPromises)
- .then(() => {
- expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
- expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('truncated information', () => {
- describe('when size is less than total', () => {
- it('shows information about truncated log', (done) => {
- response = {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 50,
- total: 100,
- };
-
- job = new Job();
-
- waitForPromises()
- .then(() => {
- expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('shows the size in KiB', (done) => {
- const size = 50;
-
- response = {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size,
- total: 100,
- };
-
- job = new Job();
-
- waitForPromises()
- .then(() => {
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${numberToHumanSize(size)}`);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('shows incremented size', (done) => {
- response = {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 50,
- total: 100,
- complete: false,
- };
-
- job = new Job();
-
- waitForPromises()
- .then(() => {
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${numberToHumanSize(50)}`);
-
- response = {
- html: '<span>Update</span>',
- status: 'success',
- append: true,
- size: 10,
- total: 100,
- complete: true,
- };
- })
- .then(() => jasmine.clock().tick(4001))
- .then(waitForPromises)
- .then(() => {
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${numberToHumanSize(60)}`);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('renders the raw link', () => {
- response = {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 50,
- total: 100,
- };
-
- job = new Job();
-
- expect(
- document.querySelector('.js-raw-link').textContent.trim(),
- ).toContain('Complete Raw');
- });
- });
-
- describe('when size is equal than total', () => {
- it('does not show the trunctated information', (done) => {
- response = {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 100,
- total: 100,
- };
-
- job = new Job();
-
- waitForPromises()
- .then(() => {
- expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
- })
- .then(done)
- .catch(done.fail);
- });
- });
- });
-
- describe('output trace', () => {
- beforeEach((done) => {
- response = {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 50,
- total: 100,
- };
-
- job = new Job();
-
- 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 $ from 'jquery';
+// import MockAdapter from 'axios-mock-adapter';
+// import axios from '~/lib/utils/axios_utils';
+// import { numberToHumanSize } from '~/lib/utils/number_utils';
+// import '~/lib/utils/datetime_utility';
+// import Job from '~/job';
+// import '~/breakpoints';
+// import waitForPromises from 'spec/helpers/wait_for_promises';
+
+// describe('Job', () => {
+// const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
+// let mock;
+// let response;
+// let job;
+
+// preloadFixtures('builds/build-with-artifacts.html.raw');
+
+// beforeEach(() => {
+// loadFixtures('builds/build-with-artifacts.html.raw');
+
+// spyOnDependency(Job, 'visitUrl');
+
+// response = {};
+
+// mock = new MockAdapter(axios);
+
+// mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]);
+// });
+
+// afterEach(() => {
+// mock.restore();
+
+// clearTimeout(job.timeout);
+// });
+
+// describe('class constructor', () => {
+// beforeEach(() => {
+// jasmine.clock().install();
+// });
+
+// afterEach(() => {
+// jasmine.clock().uninstall();
+// });
+
+// describe('running build', () => {
+// it('updates the build trace on an interval', function (done) {
+// response = {
+// html: '<span>Update<span>',
+// status: 'running',
+// state: 'newstate',
+// append: true,
+// complete: false,
+// };
+
+// job = new Job();
+
+// waitForPromises()
+// .then(() => {
+// expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+// expect(job.state).toBe('newstate');
+
+// response = {
+// html: '<span>More</span>',
+// status: 'running',
+// state: 'finalstate',
+// append: true,
+// complete: true,
+// };
+// })
+// .then(() => jasmine.clock().tick(4001))
+// .then(waitForPromises)
+// .then(() => {
+// expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
+// expect(job.state).toBe('finalstate');
+// })
+// .then(done)
+// .catch(done.fail);
+// });
+
+// it('replaces the entire build trace', (done) => {
+// response = {
+// html: '<span>Update<span>',
+// status: 'running',
+// append: false,
+// complete: false,
+// };
+
+// job = new Job();
+
+// waitForPromises()
+// .then(() => {
+// expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+
+// response = {
+// html: '<span>Different</span>',
+// status: 'running',
+// append: false,
+// };
+// })
+// .then(() => jasmine.clock().tick(4001))
+// .then(waitForPromises)
+// .then(() => {
+// expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
+// expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
+// })
+// .then(done)
+// .catch(done.fail);
+// });
+// });
+
+// describe('truncated information', () => {
+// describe('when size is less than total', () => {
+// it('shows information about truncated log', (done) => {
+// response = {
+// html: '<span>Update</span>',
+// status: 'success',
+// append: false,
+// size: 50,
+// total: 100,
+// };
+
+// job = new Job();
+
+// waitForPromises()
+// .then(() => {
+// expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
+// })
+// .then(done)
+// .catch(done.fail);
+// });
+
+// it('shows the size in KiB', (done) => {
+// const size = 50;
+
+// response = {
+// html: '<span>Update</span>',
+// status: 'success',
+// append: false,
+// size,
+// total: 100,
+// };
+
+// job = new Job();
+
+// waitForPromises()
+// .then(() => {
+// expect(
+// document.querySelector('.js-truncated-info-size').textContent.trim(),
+// ).toEqual(`${numberToHumanSize(size)}`);
+// })
+// .then(done)
+// .catch(done.fail);
+// });
+
+// it('shows incremented size', (done) => {
+// response = {
+// html: '<span>Update</span>',
+// status: 'success',
+// append: false,
+// size: 50,
+// total: 100,
+// complete: false,
+// };
+
+// job = new Job();
+
+// waitForPromises()
+// .then(() => {
+// expect(
+// document.querySelector('.js-truncated-info-size').textContent.trim(),
+// ).toEqual(`${numberToHumanSize(50)}`);
+
+// response = {
+// html: '<span>Update</span>',
+// status: 'success',
+// append: true,
+// size: 10,
+// total: 100,
+// complete: true,
+// };
+// })
+// .then(() => jasmine.clock().tick(4001))
+// .then(waitForPromises)
+// .then(() => {
+// expect(
+// document.querySelector('.js-truncated-info-size').textContent.trim(),
+// ).toEqual(`${numberToHumanSize(60)}`);
+// })
+// .then(done)
+// .catch(done.fail);
+// });
+
+// it('renders the raw link', () => {
+// response = {
+// html: '<span>Update</span>',
+// status: 'success',
+// append: false,
+// size: 50,
+// total: 100,
+// };
+
+// job = new Job();
+
+// expect(
+// document.querySelector('.js-raw-link').textContent.trim(),
+// ).toContain('Complete Raw');
+// });
+// });
+
+// describe('when size is equal than total', () => {
+// it('does not show the trunctated information', (done) => {
+// response = {
+// html: '<span>Update</span>',
+// status: 'success',
+// append: false,
+// size: 100,
+// total: 100,
+// };
+
+// job = new Job();
+
+// waitForPromises()
+// .then(() => {
+// expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
+// })
+// .then(done)
+// .catch(done.fail);
+// });
+// });
+// });
+
+// describe('output trace', () => {
+// beforeEach((done) => {
+// response = {
+// html: '<span>Update</span>',
+// status: 'success',
+// append: false,
+// size: 50,
+// total: 100,
+// };
+
+// job = new Job();
+
+// 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>');
+// });
+// });
+// });
+
+// });
diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js
index 6e0bcf801cd..e6d403dc826 100644
--- a/spec/javascripts/jobs/components/job_app_spec.js
+++ b/spec/javascripts/jobs/components/job_app_spec.js
@@ -1,75 +1,196 @@
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 createStore from '~/jobs/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from '../store/helpers';
+import job from '../mock_data';
describe('Job App ', () => {
const Component = Vue.extend(jobApp);
let store;
let vm;
-
- 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,
- };
+ let mock;
const props = {
+ endpoint: `${gl.TEST_HOST}jobs/123.json`,
+ runnerHelpUrl: 'help/runner',
runnerSettingsUrl: 'settings/ci-cd/runners',
+ terminalPath: 'jobs/123/terminal',
+ pagePath: `${gl.TEST_HOST}jobs/123`,
+ logState:
+ 'eyJvZmZzZXQiOjE3NDUxLCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowfQ%3D%3D',
};
beforeEach(() => {
+ mock = new MockAdapter(axios);
store = createStore();
});
afterEach(() => {
+ resetStore(store);
vm.$destroy();
+ mock.restore();
});
- describe('Header section', () => {
- describe('job callout message', () => {
- it('should not render the reason when reason is absent', () => {
- store.dispatch('receiveJobSuccess', job);
+ describe('while loading', () => {
+ beforeEach(() => {
+ mock.onGet(props.endpoint).reply(200, 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, {
props,
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', () => {
- store.dispatch(
- 'receiveJobSuccess',
+ it('does not renders stuck block when there are no runners', done => {
+ mock.onGet(props.endpoint).replyOnce(
+ 200,
Object.assign({}, job, {
- callout_message: 'There is an unknown failure, please try again',
+ runners: { available: true },
}),
);
@@ -78,246 +199,324 @@ describe('Job App ', () => {
store,
});
- expect(vm.shouldRenderCalloutMessage).toBe(true);
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-stuck')).toBeNull();
+
+ done();
+ }, 0);
});
});
- describe('triggered job', () => {
- beforeEach(() => {
- store.dispatch('receiveJobSuccess', job);
+ describe('environments block', () => {
+ it('renders environment block when job has environment', done => {
+ mock.onGet(props.endpoint).replyOnce(
+ 200,
+ Object.assign({}, job, {
+ deployment_status: {
+ environment: {
+ environment_path: '/path',
+ name: 'foo',
+ },
+ },
+ }),
+ );
vm = mountComponentWithStore(Component, {
props,
store,
});
- });
- it('should render provided job information', () => {
- expect(
- vm.$el
- .querySelector('.header-main-content')
- .textContent.replace(/\s+/g, ' ')
- .trim(),
- ).toEqual('failed Job #123 triggered 2 days ago by Foo');
- });
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-environment')).not.toBeNull();
- it('should render new issue link', () => {
- expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
- job.new_issue_path,
- );
+ done();
+ }, 0);
});
- });
- describe('created job', () => {
- it('should render created key', () => {
- store.dispatch('receiveJobSuccess', Object.assign({}, job, { started: false }));
+ it('does not render environment block when job has environment', done => {
+ mock.onGet(props.endpoint).replyOnce(200, job);
vm = mountComponentWithStore(Component, {
props,
store,
});
- expect(
- vm.$el
- .querySelector('.header-main-content')
- .textContent.replace(/\s+/g, ' ')
- .trim(),
- ).toEqual('failed Job #123 created 3 weeks ago by Foo');
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-environment')).toBeNull();
+ done();
+ }, 0);
});
});
- });
- describe('stuck block', () => {
- it('renders stuck block when there are no runners', () => {
- store.dispatch(
- 'receiveJobSuccess',
- Object.assign({}, job, {
- status: {
- group: 'pending',
- icon: 'status_pending',
- label: 'pending',
- text: 'pending',
- details_path: 'path',
- },
- }),
- );
-
- vm = mountComponentWithStore(Component, {
- props,
- store,
- });
+ describe('erased block', () => {
+ it('renders erased block when `erased` is true', done => {
+ mock.onGet(props.endpoint).replyOnce(
+ 200,
+ Object.assign({}, job, {
+ erased_by: {
+ username: 'root',
+ web_url: 'gitlab.com/root',
+ },
+ erased_at: '2016-11-07T11:11:16.525Z',
+ }),
+ );
- 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', () => {
- store.dispatch(
- 'receiveJobSuccess',
- Object.assign({}, job, {
- status: {
- group: 'pending',
- icon: 'status_pending',
- label: 'pending',
- text: 'pending',
- details_path: 'path',
- },
- }),
- );
-
- vm = mountComponentWithStore(Component, {
- props,
- store,
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-erased-block')).not.toBeNull();
+
+ done();
+ }, 0);
});
- 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', () => {
- store.dispatch('receiveJobSuccess', Object.assign({}, job, { runners: { available: true } }));
+ vm = mountComponentWithStore(Component, {
+ props,
+ store,
+ });
- vm = mountComponentWithStore(Component, {
- props,
- store,
- });
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-erased-block')).toBeNull();
- expect(vm.$el.querySelector('.js-job-stuck')).toBeNull();
+ done();
+ }, 0);
+ });
});
- });
- describe('environments block', () => {
- it('renders environment block when job has environment', () => {
- store.dispatch(
- 'receiveJobSuccess',
- Object.assign({}, job, {
- deployment_status: {
- environment: {
- environment_path: '/path',
- name: 'foo',
+ describe('empty states block', () => {
+ it('renders empty state when job does not have trace and is not running', done => {
+ mock.onGet(props.endpoint).replyOnce(
+ 200,
+ 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, {
- props,
- store,
+ vm = mountComponentWithStore(Component, {
+ props,
+ 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', () => {
- store.dispatch('receiveJobSuccess', job);
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull();
- vm = mountComponentWithStore(Component, {
- props,
- store,
+ done();
+ }, 0);
});
- 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', () => {
- it('renders erased block when `erased` is true', () => {
- store.dispatch(
- 'receiveJobSuccess',
- Object.assign({}, job, {
- erased_by: {
- username: 'root',
- web_url: 'gitlab.com/root',
- },
- erased_at: '2016-11-07T11:11:16.525Z',
- }),
- );
-
- vm = mountComponentWithStore(Component, {
- props,
- store,
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull();
+
+ done();
+ }, 0);
});
+ });
+ });
- 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', () => {
- store.dispatch('receiveJobSuccess', Object.assign({}, job, { erased_at: null }));
+ describe('with append flag', () => {
+ 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, {
- props,
- store,
- });
+ vm = mountComponentWithStore(Component, {
+ props,
+ store,
+ });
- expect(vm.$el.querySelector('.js-job-erased-block')).toBeNull();
- });
- });
+ vm.$store.state.trace = 'Update';
- describe('empty states block', () => {
- it('renders empty state when job does not have trace and is not running', () => {
- 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',
- },
- },
- }),
- );
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain('Update');
- vm = mountComponentWithStore(Component, {
- props,
- store,
+ done();
+ }, 0);
});
+ });
+
+ 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', () => {
- store.dispatch(
- 'receiveJobSuccess',
- Object.assign({}, job, {
- has_trace: false,
- status: {
- group: 'running',
- icon: 'status_running',
- label: 'running',
- text: 'running',
- details_path: 'path',
- },
- }),
- );
-
- vm = mountComponentWithStore(Component, {
- props,
- store,
+ describe('truncated information', () => {
+ describe('when size is less than total', () => {
+ it('shows information about truncated log', done => {
+ 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,
+ });
+
+ 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', () => {
- store.dispatch('receiveJobSuccess', Object.assign({}, job, { has_trace: true }));
+ describe('trace controls', () => {
+ 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, {
- props,
- store,
+ it('should render link to raw ouput', done => {
+ setTimeout(() => {
+ 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);
+ });
});
});
});
diff --git a/spec/javascripts/jobs/components/job_log_spec.js b/spec/javascripts/jobs/components/job_log_spec.js
index 1011512360d..413a96e502a 100644
--- a/spec/javascripts/jobs/components/job_log_spec.js
+++ b/spec/javascripts/jobs/components/job_log_spec.js
@@ -1,21 +1,32 @@
import Vue from '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', () => {
const Component = Vue.extend(component);
+ let store;
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>';
+ beforeEach(() => {
+ store = createStore();
+ });
+
afterEach(() => {
+ resetStore(store);
vm.$destroy();
});
it('renders provided trace', () => {
- vm = mountComponent(Component, {
- trace,
- isComplete: true,
+ vm = mountComponentWithStore(Component, {
+ props: {
+ trace,
+ isComplete: true,
+ },
+ store,
});
expect(vm.$el.querySelector('code').textContent).toContain('Running with gitlab-runner 11.1.0 (081978aa)');
@@ -23,9 +34,12 @@ describe('Job Log', () => {
describe('while receiving trace', () => {
it('renders animation', () => {
- vm = mountComponent(Component, {
- trace,
- isComplete: true,
+ vm = mountComponentWithStore(Component, {
+ props: {
+ trace,
+ isComplete: false,
+ },
+ store,
});
expect(vm.$el.querySelector('.js-log-animation')).not.toBeNull();
@@ -34,9 +48,12 @@ describe('Job Log', () => {
describe('when build trace has finishes', () => {
it('does not render animation', () => {
- vm = mountComponent(Component, {
- trace,
- isComplete: false,
+ vm = mountComponentWithStore(Component, {
+ props: {
+ trace,
+ isComplete: true,
+ },
+ store,
});
expect(vm.$el.querySelector('.js-log-animation')).toBeNull();
diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js
index ca4f62bd6ef..460a2e1b5da 100644
--- a/spec/javascripts/jobs/components/sidebar_spec.js
+++ b/spec/javascripts/jobs/components/sidebar_spec.js
@@ -18,15 +18,6 @@ describe('Sidebar details block', () => {
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', () => {
it('should not render a retry button', () => {
const copy = Object.assign({}, job);
@@ -52,12 +43,12 @@ describe('Sidebar details block', () => {
describe('with terminal path', () => {
it('renders terminal link', () => {
- store.dispatch('receiveJobSuccess', job);
+ store.dispatch(
+ 'receiveJobSuccess',
+ Object.assign({}, job, { terminal_path: 'job/43123/terminal' }),
+ );
vm = mountComponentWithStore(SidebarComponent, {
store,
- props: {
- terminalPath: 'job/43123/terminal',
- },
});
expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull();
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 4269b42e8b6..ca6fbabeeb6 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -31,6 +31,7 @@ export default {
},
coverage: 20,
erased_at: threeWeeksAgo.toISOString(),
+ erased: false,
duration: 6.785563,
tags: ['tag'],
user: {
@@ -131,6 +132,7 @@ export default {
path: '/root/ci-mock/merge_requests/2',
},
raw_path: '/root/ci-mock/builds/4757/raw',
+ has_trace: true,
};
export const stages = [
diff --git a/spec/javascripts/jobs/store/actions_spec.js b/spec/javascripts/jobs/store/actions_spec.js
index bc410ae614c..76af08b367d 100644
--- a/spec/javascripts/jobs/store/actions_spec.js
+++ b/spec/javascripts/jobs/store/actions_spec.js
@@ -2,9 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import {
setJobEndpoint,
- setTraceEndpoint,
- setStagesEndpoint,
- setJobsEndpoint,
+ setTraceOptions,
clearEtagPoll,
stopPolling,
requestJob,
@@ -18,10 +16,6 @@ import {
stopPollingTrace,
receiveTraceSuccess,
receiveTraceError,
- fetchFavicon,
- requestStatusFavicon,
- receiveStatusFaviconSuccess,
- requestStatusFaviconError,
requestStages,
fetchStages,
receiveStagesSuccess,
@@ -30,6 +24,9 @@ import {
fetchJobsForStage,
receiveJobsForStageSuccess,
receiveJobsForStageError,
+ hideSidebar,
+ showSidebar,
+ toggleSidebar,
} from '~/jobs/store/actions';
import state from '~/jobs/store/state';
import * as types from '~/jobs/store/mutation_types';
@@ -56,45 +53,75 @@ describe('Job State actions', () => {
});
});
- describe('setTraceEndpoint', () => {
- it('should commit SET_TRACE_ENDPOINT mutation', done => {
+ describe('setTraceOptions', () => {
+ it('should commit SET_TRACE_OPTIONS mutation', done => {
testAction(
- setTraceEndpoint,
- 'job/872324/trace.json',
+ setTraceOptions,
+ { pagePath: 'job/872324/trace.json' },
mockedState,
- [{ type: types.SET_TRACE_ENDPOINT, payload: 'job/872324/trace.json' }],
+ [{ type: types.SET_TRACE_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }],
[],
done,
);
});
});
- describe('setStagesEndpoint', () => {
- it('should commit SET_STAGES_ENDPOINT mutation', done => {
+ describe('hideSidebar', () => {
+ it('should commit HIDE_SIDEBAR mutation', done => {
testAction(
- setStagesEndpoint,
- 'job/872324/stages.json',
+ hideSidebar,
+ null,
mockedState,
- [{ type: types.SET_STAGES_ENDPOINT, payload: 'job/872324/stages.json' }],
+ [{ type: types.HIDE_SIDEBAR }],
[],
done,
);
});
});
- describe('setJobsEndpoint', () => {
- it('should commit SET_JOBS_ENDPOINT mutation', done => {
+ describe('showSidebar', () => {
+ it('should commit HIDE_SIDEBAR mutation', done => {
testAction(
- setJobsEndpoint,
- 'job/872324/stages/build.json',
+ showSidebar,
+ null,
mockedState,
- [{ type: types.SET_JOBS_ENDPOINT, payload: 'job/872324/stages/build.json' }],
+ [{ type: types.SHOW_SIDEBAR }],
[],
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', () => {
it('should commit REQUEST_JOB mutation', done => {
testAction(requestJob, null, mockedState, [{ type: types.REQUEST_JOB }], [], done);
@@ -183,14 +210,14 @@ describe('Job State actions', () => {
});
describe('scrollTop', () => {
- it('should commit SCROLL_TO_TOP mutation', done => {
- testAction(scrollTop, null, mockedState, [{ type: types.SCROLL_TO_TOP }], [], done);
+ it('should dispatch toggleScrollButtons action', done => {
+ testAction(scrollTop, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
});
});
describe('scrollBottom', () => {
- it('should commit SCROLL_TO_BOTTOM mutation', done => {
- testAction(scrollBottom, null, mockedState, [{ type: types.SCROLL_TO_BOTTOM }], [], done);
+ it('should dispatch toggleScrollButtons action', done => {
+ testAction(scrollBottom, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
});
});
@@ -215,7 +242,7 @@ describe('Job State actions', () => {
});
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, {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: true,
@@ -228,10 +255,8 @@ describe('Job State actions', () => {
[],
[
{
- type: 'requestTrace',
- },
- {
- type: 'fetchFavicon',
+ type: 'toggleScrollisInBottom',
+ payload: true,
},
{
payload: {
@@ -262,9 +287,6 @@ describe('Job State actions', () => {
[],
[
{
- type: 'requestTrace',
- },
- {
type: 'receiveTraceError',
},
],
@@ -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', () => {
it('should commit REQUEST_STAGES mutation ', done => {
testAction(requestStages, null, mockedState, [{ type: types.REQUEST_STAGES }], [], done);
diff --git a/spec/javascripts/jobs/store/helpers.js b/spec/javascripts/jobs/store/helpers.js
new file mode 100644
index 00000000000..81a769b4a6e
--- /dev/null
+++ b/spec/javascripts/jobs/store/helpers.js
@@ -0,0 +1,6 @@
+import state from '~/jobs/store/state';
+
+// eslint-disable-next-line import/prefer-default-export
+export const resetStore = store => {
+ store.replaceState(state());
+};
diff --git a/spec/javascripts/jobs/store/mutations_spec.js b/spec/javascripts/jobs/store/mutations_spec.js
index c058a3b3d51..9bbf4ba8951 100644
--- a/spec/javascripts/jobs/store/mutations_spec.js
+++ b/spec/javascripts/jobs/store/mutations_spec.js
@@ -20,27 +20,19 @@ describe('Jobs Store Mutations', () => {
});
});
- describe('REQUEST_STATUS_FAVICON', () => {
- it('should set fetchingStatusFavicon to true', () => {
- mutations[types.REQUEST_STATUS_FAVICON](stateCopy);
+ describe('HIDE_SIDEBAR', () => {
+ it('should set isSidebarOpen to false', () => {
+ mutations[types.HIDE_SIDEBAR](stateCopy);
- expect(stateCopy.fetchingStatusFavicon).toEqual(true);
+ expect(stateCopy.isSidebarOpen).toEqual(false);
});
});
- describe('RECEIVE_STATUS_FAVICON_SUCCESS', () => {
- it('should set fetchingStatusFavicon to false', () => {
- mutations[types.RECEIVE_STATUS_FAVICON_SUCCESS](stateCopy);
+ describe('SHOW_SIDEBAR', () => {
+ it('should set isSidebarOpen to true', () => {
+ mutations[types.SHOW_SIDEBAR](stateCopy);
- expect(stateCopy.fetchingStatusFavicon).toEqual(false);
- });
- });
-
- describe('RECEIVE_STATUS_FAVICON_ERROR', () => {
- it('should set fetchingStatusFavicon to false', () => {
- mutations[types.RECEIVE_STATUS_FAVICON_ERROR](stateCopy);
-
- expect(stateCopy.fetchingStatusFavicon).toEqual(false);
+ expect(stateCopy.isSidebarOpen).toEqual(true);
});
});
@@ -101,9 +93,7 @@ describe('Jobs Store Mutations', () => {
it('resets trace state and sets error to true', () => {
mutations[types.RECEIVE_TRACE_ERROR](stateCopy);
- expect(stateCopy.isLoadingTrace).toEqual(false);
expect(stateCopy.isTraceComplete).toEqual(true);
- expect(stateCopy.hasTraceError).toEqual(true);
});
});
@@ -156,39 +146,10 @@ describe('Jobs Store Mutations', () => {
mutations[types.RECEIVE_JOB_ERROR](stateCopy);
expect(stateCopy.isLoading).toEqual(false);
- expect(stateCopy.hasError).toEqual(true);
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', () => {
it('sets isLoadingStages to true', () => {
mutations[types.REQUEST_STAGES](stateCopy);
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
index f17818c17c7..43750c74b99 100644
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -94,7 +94,7 @@ describe('Header CI Component', () => {
});
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();
});
});