diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 11:27:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 11:27:35 +0300 |
commit | 7e9c479f7de77702622631cff2628a9c8dcbc627 (patch) | |
tree | c8f718a08e110ad7e1894510980d2155a6549197 /app/assets/javascripts/jobs | |
parent | e852b0ae16db4052c1c567d9efa4facc81146e88 (diff) |
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/jobs')
11 files changed, 371 insertions, 163 deletions
diff --git a/app/assets/javascripts/jobs/components/job_retry_forward_deployment_modal.vue b/app/assets/javascripts/jobs/components/job_retry_forward_deployment_modal.vue new file mode 100644 index 00000000000..5ce9d08035d --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_retry_forward_deployment_modal.vue @@ -0,0 +1,66 @@ +<script> +import { GlLink, GlModal } from '@gitlab/ui'; +import { JOB_RETRY_FORWARD_DEPLOYMENT_MODAL } from '../constants'; + +export default { + name: 'JobRetryForwardDeploymentModal', + components: { + GlLink, + GlModal, + }, + i18n: { + ...JOB_RETRY_FORWARD_DEPLOYMENT_MODAL, + }, + props: { + modalId: { + type: String, + required: true, + }, + href: { + type: String, + required: true, + }, + }, + inject: { + retryOutdatedJobDocsUrl: { + default: '', + }, + }, + data() { + return { + primaryProps: { + text: this.$options.i18n.primaryText, + attributes: [ + { + 'data-method': 'post', + 'data-testid': 'retry-button-modal', + href: this.href, + variant: 'danger', + }, + ], + }, + cancelProps: { + text: this.$options.i18n.cancel, + attributes: [{ category: 'secondary', variant: 'default' }], + }, + }; + }, +}; +</script> + +<template> + <gl-modal + :action-cancel="cancelProps" + :action-primary="primaryProps" + :modal-id="modalId" + :title="$options.i18n.title" + > + <p> + {{ $options.i18n.info }} + <gl-link v-if="retryOutdatedJobDocsUrl" :href="retryOutdatedJobDocsUrl" target="_blank"> + {{ $options.i18n.moreInfo }} + </gl-link> + </p> + <p>{{ $options.i18n.areYouSure }}</p> + </gl-modal> +</template> diff --git a/app/assets/javascripts/jobs/components/job_sidebar_retry_button.vue b/app/assets/javascripts/jobs/components/job_sidebar_retry_button.vue new file mode 100644 index 00000000000..258b8cadd63 --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_sidebar_retry_button.vue @@ -0,0 +1,45 @@ +<script> +import { GlButton, GlLink, GlModalDirective } from '@gitlab/ui'; +import { mapGetters } from 'vuex'; +import { JOB_SIDEBAR } from '../constants'; + +export default { + name: 'JobSidebarRetryButton', + i18n: { + retryLabel: JOB_SIDEBAR.retry, + }, + components: { + GlButton, + GlLink, + }, + directives: { + GlModal: GlModalDirective, + }, + props: { + modalId: { + type: String, + required: true, + }, + href: { + type: String, + required: true, + }, + }, + computed: { + ...mapGetters(['hasForwardDeploymentFailure']), + }, +}; +</script> +<template> + <gl-button + v-if="hasForwardDeploymentFailure" + v-gl-modal="modalId" + :aria-label="$options.i18n.retryLabel" + category="primary" + variant="info" + >{{ $options.i18n.retryLabel }}</gl-button + > + <gl-link v-else :href="href" data-method="post" rel="nofollow" + >{{ $options.i18n.retryLabel }} + </gl-link> +</template> diff --git a/app/assets/javascripts/jobs/components/jobs_container.vue b/app/assets/javascripts/jobs/components/jobs_container.vue index 951bcb36600..df64b6422c7 100644 --- a/app/assets/javascripts/jobs/components/jobs_container.vue +++ b/app/assets/javascripts/jobs/components/jobs_container.vue @@ -24,7 +24,7 @@ export default { }; </script> <template> - <div class="js-jobs-container builds-container"> + <div class="builds-container"> <job-container-item v-for="job in jobs" :key="job.id" diff --git a/app/assets/javascripts/jobs/components/log/line.vue b/app/assets/javascripts/jobs/components/log/line.vue index e68d5b8eda4..affaddcdee2 100644 --- a/app/assets/javascripts/jobs/components/log/line.vue +++ b/app/assets/javascripts/jobs/components/log/line.vue @@ -1,4 +1,6 @@ <script> +import { linkRegex } from '../../utils'; + import LineNumber from './line_number.vue'; export default { @@ -16,15 +18,46 @@ export default { render(h, { props }) { const { line, path } = props; - const chars = line.content.map(content => { - return h( - 'span', - { - class: ['gl-white-space-pre-wrap', content.style], - }, - content.text, - ); - }); + let chars; + if (gon?.features?.ciJobLineLinks) { + chars = line.content.map(content => { + return h( + 'span', + { + class: ['gl-white-space-pre-wrap', content.style], + }, + // Simple "tokenization": Split text in chunks of text + // which alternate between text and urls. + content.text.split(linkRegex).map(chunk => { + // Return normal string for non-links + if (!chunk.match(linkRegex)) { + return chunk; + } + return h( + 'a', + { + attrs: { + href: chunk, + class: 'gl-reset-color! gl-text-decoration-underline', + rel: 'nofollow noopener noreferrer', // eslint-disable-line @gitlab/require-i18n-strings + }, + }, + chunk, + ); + }), + ); + }); + } else { + chars = line.content.map(content => { + return h( + 'span', + { + class: ['gl-white-space-pre-wrap', content.style], + }, + content.text, + ); + }); + } return h('div', { class: 'js-line log-line' }, [ h(LineNumber, { diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index 8701e05a01f..0789bb54f0f 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -1,33 +1,40 @@ <script> import { isEmpty } from 'lodash'; -import { mapActions, mapState } from 'vuex'; -import { GlLink, GlButton, GlIcon } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; -import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { mapActions, mapGetters, mapState } from 'vuex'; +import { GlButton, GlIcon, GlLink } from '@gitlab/ui'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; -import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; -import DetailRow from './sidebar_detail_row.vue'; import ArtifactsBlock from './artifacts_block.vue'; +import JobSidebarRetryButton from './job_sidebar_retry_button.vue'; +import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue'; import TriggerBlock from './trigger_block.vue'; import CommitBlock from './commit_block.vue'; import StagesDropdown from './stages_dropdown.vue'; import JobsContainer from './jobs_container.vue'; +import JobSidebarDetailsContainer from './sidebar_job_details_container.vue'; +import { JOB_SIDEBAR } from '../constants'; + +export const forwardDeploymentFailureModalId = 'forward-deployment-failure'; export default { name: 'JobSidebar', + i18n: { + ...JOB_SIDEBAR, + }, + forwardDeploymentFailureModalId, components: { ArtifactsBlock, CommitBlock, - DetailRow, + GlButton, + GlLink, GlIcon, - TriggerBlock, - StagesDropdown, JobsContainer, - GlLink, - GlButton, + JobSidebarRetryButton, + JobRetryForwardDeploymentModal, + JobSidebarDetailsContainer, + StagesDropdown, TooltipOnTruncate, + TriggerBlock, }, - mixins: [timeagoMixin], props: { artifactHelpUrl: { type: String, @@ -41,54 +48,14 @@ export default { }, }, computed: { + ...mapGetters(['hasForwardDeploymentFailure']), ...mapState(['job', 'stages', 'jobs', 'selectedStage']), - coverage() { - return `${this.job.coverage}%`; - }, - duration() { - return timeIntervalInWords(this.job.duration); - }, - queued() { - return timeIntervalInWords(this.job.queued); - }, - runnerId() { - return `${this.job.runner.description} (#${this.job.runner.id})`; - }, retryButtonClass() { - let className = 'js-retry-button btn btn-retry'; + let className = 'btn btn-retry'; className += this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary'; return className; }, - hasTimeout() { - return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null; - }, - timeout() { - if (this.job.metadata == null) { - return ''; - } - - let t = this.job.metadata.timeout_human_readable; - if (this.job.metadata.timeout_source !== '') { - t += sprintf(__(` (from %{timeoutSource})`), { - timeoutSource: this.job.metadata.timeout_source, - }); - } - - return t; - }, - renderBlock() { - return ( - this.job.duration || - this.job.finished_at || - this.job.erased_at || - this.job.queued || - this.hasTimeout || - this.job.runner || - this.job.coverage || - this.job.tags.length - ); - }, hasArtifact() { return !isEmpty(this.job.artifact); }, @@ -96,16 +63,13 @@ export default { return !isEmpty(this.job.trigger); }, hasStages() { - return ( - (this.job && - this.job.pipeline && - this.job.pipeline.stages && - this.job.pipeline.stages.length > 0) || - false - ); + return this.job?.pipeline?.stages?.length > 0; }, commit() { - return this.job.pipeline && this.job.pipeline.commit ? this.job.pipeline.commit : {}; + return this.job?.pipeline?.commit || {}; + }, + shouldShowJobRetryForwardDeploymentModal() { + return this.job.retry_path && this.hasForwardDeploymentFailure; }, }, methods: { @@ -124,29 +88,29 @@ export default { </h4> </tooltip-on-truncate> <div class="flex-grow-1 flex-shrink-0 text-right"> - <gl-link + <job-sidebar-retry-button v-if="job.retry_path" :class="retryButtonClass" :href="job.retry_path" - data-method="post" + :modal-id="$options.forwardDeploymentFailureModalId" data-qa-selector="retry_button" - rel="nofollow" - >{{ __('Retry') }}</gl-link - > + data-testid="retry-button" + /> <gl-link v-if="job.cancel_path" :href="job.cancel_path" - class="js-cancel-job btn btn-default" + class="btn btn-default" data-method="post" + data-testid="cancel-button" rel="nofollow" - >{{ __('Cancel') }}</gl-link - > + >{{ $options.i18n.cancel }} + </gl-link> </div> <gl-button - :aria-label="__('Toggle Sidebar')" - class="d-md-none gl-ml-2 js-sidebar-build-toggle" + :aria-label="$options.i18n.toggleSidebar" category="tertiary" + class="gl-display-md-none gl-ml-2 js-sidebar-build-toggle" icon="chevron-double-lg-right" @click="toggleSidebar" /> @@ -158,77 +122,43 @@ export default { :href="job.new_issue_path" class="btn btn-success btn-inverted float-left mr-2" data-testid="job-new-issue" - >{{ __('New issue') }}</gl-link - > + >{{ $options.i18n.newIssue }} + </gl-link> <gl-link v-if="job.terminal_path" :href="job.terminal_path" - class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left" + class="btn btn-primary btn-inverted visible-md-block visible-lg-block float-left" target="_blank" + data-testid="terminal-link" > - {{ __('Debug') }} <gl-icon name="external-link" :size="14" /> + {{ $options.i18n.debug }} + <gl-icon :size="14" name="external-link" /> </gl-link> </div> - - <div v-if="renderBlock" class="block"> - <detail-row - v-if="job.duration" - :value="duration" - class="js-job-duration" - title="Duration" - /> - <detail-row - v-if="job.finished_at" - :value="timeFormatted(job.finished_at)" - class="js-job-finished" - title="Finished" - /> - <detail-row - v-if="job.erased_at" - :value="timeFormatted(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="font-weight-bold">{{ __('Tags:') }}</span> - <span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary mr-1">{{ - tag - }}</span> - </p> - </div> - + <job-sidebar-details-container :runner-help-url="runnerHelpUrl" /> <artifacts-block v-if="hasArtifact" :artifact="job.artifact" :help-url="artifactHelpUrl" /> <trigger-block v-if="hasTriggers" :trigger="job.trigger" /> <commit-block - :is-last-block="hasStages" :commit="commit" + :is-last-block="hasStages" :merge-request="job.merge_request" /> <stages-dropdown - :stages="stages" + v-if="job.pipeline" :pipeline="job.pipeline" :selected-stage="selectedStage" + :stages="stages" @requestSidebarStageDropdown="fetchJobsForStage" /> </div> - <jobs-container v-if="jobs.length" :jobs="jobs" :job-id="job.id" /> + <jobs-container v-if="jobs.length" :job-id="job.id" :jobs="jobs" /> </div> + <job-retry-forward-deployment-modal + v-if="shouldShowJobRetryForwardDeploymentModal" + :modal-id="$options.forwardDeploymentFailureModalId" + :href="job.retry_path" + /> </aside> </template> diff --git a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue new file mode 100644 index 00000000000..8ad1008278e --- /dev/null +++ b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue @@ -0,0 +1,102 @@ +<script> +import { mapState } from 'vuex'; +import DetailRow from './sidebar_detail_row.vue'; +import { __, sprintf } from '~/locale'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; + +export default { + name: 'JobSidebarDetailsContainer', + components: { + DetailRow, + }, + mixins: [timeagoMixin], + props: { + runnerHelpUrl: { + type: String, + required: false, + default: '', + }, + }, + computed: { + ...mapState(['job']), + coverage() { + return `${this.job.coverage}%`; + }, + duration() { + return timeIntervalInWords(this.job.duration); + }, + erasedAt() { + return this.timeFormatted(this.job.erased_at); + }, + finishedAt() { + return this.timeFormatted(this.job.finished_at); + }, + hasTags() { + return this.job?.tags?.length; + }, + hasTimeout() { + return this.job?.metadata?.timeout_human_readable ?? false; + }, + hasAnyDetail() { + return Boolean( + this.job.duration || + this.job.finished_at || + this.job.erased_at || + this.job.queued || + this.job.runner || + this.job.coverage, + ); + }, + queued() { + return timeIntervalInWords(this.job.queued); + }, + runnerId() { + return `${this.job.runner.description} (#${this.job.runner.id})`; + }, + shouldRenderBlock() { + return Boolean(this.hasAnyDetail || this.hasTimeout || this.hasTags); + }, + timeout() { + return `${this.job?.metadata?.timeout_human_readable}${this.timeoutSource}`; + }, + timeoutSource() { + if (!this.job?.metadata?.timeout_source) { + return ''; + } + + return sprintf(__(` (from %{timeoutSource})`), { + timeoutSource: this.job.metadata.timeout_source, + }); + }, + }, +}; +</script> + +<template> + <div v-if="shouldRenderBlock" class="block"> + <detail-row v-if="job.duration" :value="duration" title="Duration" /> + <detail-row + v-if="job.finished_at" + :value="finishedAt" + data-testid="job-finished" + title="Finished" + /> + <detail-row v-if="job.erased_at" :value="erasedAt" title="Erased" /> + <detail-row v-if="job.queued" :value="queued" title="Queued" /> + <detail-row + v-if="hasTimeout" + :help-url="runnerHelpUrl" + :value="timeout" + data-testid="job-timeout" + title="Timeout" + /> + <detail-row v-if="job.runner" :value="runnerId" title="Runner" /> + <detail-row v-if="job.coverage" :value="coverage" title="Coverage" /> + + <p v-if="hasTags" class="build-detail-row" data-testid="job-tags"> + <span class="font-weight-bold">{{ __('Tags:') }}</span> + <span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary mr-1">{{ tag }}</span> + </p> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index 116331d9549..aeae9f26ed3 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -1,11 +1,13 @@ <script> import { isEmpty } from 'lodash'; -import { GlLink } from '@gitlab/ui'; +import { GlLink, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; export default { components: { CiIcon, + GlDropdown, + GlDropdownItem, GlLink, }, props: { @@ -78,20 +80,15 @@ export default { </template> </div> - <button - type="button" - data-toggle="dropdown" - class="js-selected-stage dropdown-menu-toggle gl-mt-3" - > - {{ selectedStage }} <i class="fa fa-chevron-down"></i> - </button> - - <ul class="dropdown-menu"> - <li v-for="stage in stages" :key="stage.name"> - <button type="button" class="js-stage-item stage-item" @click="onStageClick(stage)"> - {{ stage.name }} - </button> - </li> - </ul> + <gl-dropdown :text="selectedStage" class="js-selected-stage gl-w-full gl-mt-3"> + <gl-dropdown-item + v-for="stage in stages" + :key="stage.name" + class="js-stage-item stage-item" + @click="onStageClick(stage)" + > + {{ stage.name }} + </gl-dropdown-item> + </gl-dropdown> </div> </template> diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js new file mode 100644 index 00000000000..d0d625d794d --- /dev/null +++ b/app/assets/javascripts/jobs/constants.js @@ -0,0 +1,24 @@ +import { __, s__ } from '~/locale'; + +const cancel = __('Cancel'); +const moreInfo = __('More information'); + +export const JOB_SIDEBAR = { + cancel, + debug: __('Debug'), + newIssue: __('New issue'), + retry: __('Retry'), + toggleSidebar: __('Toggle Sidebar'), +}; + +export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = { + cancel, + info: s__( + `Jobs|You're about to retry a job that failed because it attempted to deploy code that is older than the latest deployment. + Retrying this job could result in overwriting the environment with the older source code.`, + ), + areYouSure: s__('Jobs|Are you sure you want to proceed?'), + moreInfo, + primaryText: __('Retry job'), + title: s__('Jobs|Are you sure you want to retry this job?'), +}; diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index 6e15360b66c..1ad6292a030 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -10,27 +10,31 @@ export default () => { // Let's start initializing the store (i.e. fetching data) right away store.dispatch('init', element.dataset); + const { + artifactHelpUrl, + deploymentHelpUrl, + runnerHelpUrl, + runnerSettingsUrl, + variablesSettingsUrl, + subscriptionsMoreMinutesUrl, + endpoint, + pagePath, + logState, + buildStatus, + projectPath, + retryOutdatedJobDocsUrl, + } = element.dataset; + return new Vue({ el: element, store, components: { JobApp, }, + provide: { + retryOutdatedJobDocsUrl, + }, render(createElement) { - const { - artifactHelpUrl, - deploymentHelpUrl, - runnerHelpUrl, - runnerSettingsUrl, - variablesSettingsUrl, - subscriptionsMoreMinutesUrl, - endpoint, - pagePath, - logState, - buildStatus, - projectPath, - } = element.dataset; - return createElement('job-app', { props: { artifactHelpUrl, diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index dc4a3578a86..8c2d1dd8ab2 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -3,8 +3,11 @@ import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at); +export const hasForwardDeploymentFailure = state => + state?.job?.failure_reason === 'forward_deployment_failure'; + export const hasUnmetPrerequisitesFailure = state => - state.job && state.job.failure_reason && state.job.failure_reason === 'unmet_prerequisites'; + state?.job?.failure_reason === 'unmet_prerequisites'; export const shouldRenderCalloutMessage = state => !isEmpty(state.job.status) && !isEmpty(state.job.callout_message); diff --git a/app/assets/javascripts/jobs/utils.js b/app/assets/javascripts/jobs/utils.js new file mode 100644 index 00000000000..28a125b2b8f --- /dev/null +++ b/app/assets/javascripts/jobs/utils.js @@ -0,0 +1,4 @@ +// capture anything starting with http:// or https:// +// up until a disallowed character or whitespace +export const linkRegex = /(https?:\/\/[^"<>\\^`{|}\s]+)/g; +export default { linkRegex }; |