diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2018-10-17 10:34:19 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-10-17 10:34:19 +0000 |
commit | 5ed91cf81bcc459ad65357c128b955e10ddce284 (patch) | |
tree | 77c4b367c9c2d1a34a6eb1dafeb1040cb97904a3 /app | |
parent | 712f41e15cb61b8804f41afddfbe5f57106248a1 (diff) | |
download | gitlab-ce-5ed91cf81bcc459ad65357c128b955e10ddce284.tar.gz |
Resolve "Integrate new vue+vuex code base with new API and remove old haml code"
Diffstat (limited to 'app')
20 files changed, 635 insertions, 667 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 } } |