summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/jobs
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 /app/assets/javascripts/jobs
parent712f41e15cb61b8804f41afddfbe5f57106248a1 (diff)
downloadgitlab-ce-5ed91cf81bcc459ad65357c128b955e10ddce284.tar.gz
Resolve "Integrate new vue+vuex code base with new API and remove old haml code"
Diffstat (limited to 'app/assets/javascripts/jobs')
-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
12 files changed, 608 insertions, 406 deletions
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>