diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 09:45:46 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 09:45:46 +0000 |
commit | a7b3560714b4d9cc4ab32dffcd1f74a284b93580 (patch) | |
tree | 7452bd5c3545c2fa67a28aa013835fb4fa071baf /app/assets/javascripts/pipelines | |
parent | ee9173579ae56a3dbfe5afe9f9410c65bb327ca7 (diff) | |
download | gitlab-ce-a7b3560714b4d9cc4ab32dffcd1f74a284b93580.tar.gz |
Add latest changes from gitlab-org/gitlab@14-8-stable-eev14.8.0-rc42
Diffstat (limited to 'app/assets/javascripts/pipelines')
21 files changed, 508 insertions, 101 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue index 12c3f9a7f40..795ba91a164 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -59,7 +59,11 @@ export default { </script> <template> <!-- eslint-disable @gitlab/vue-no-data-toggle --> - <div :id="computedJobId" class="ci-job-dropdown-container dropdown dropright"> + <div + :id="computedJobId" + class="ci-job-dropdown-container dropdown dropright" + data-qa-selector="job_dropdown_container" + > <button type="button" data-toggle="dropdown" @@ -79,7 +83,10 @@ export default { </div> </button> - <ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown"> + <ul + class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown" + data-qa-selector="jobs_dropdown_menu" + > <li class="scrollable-menu"> <ul> <li v-for="job in group.jobs" :key="job.id"> diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index ee58dcc4882..795b95421c7 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -2,7 +2,7 @@ import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; -import { sprintf } from '~/locale'; +import { sprintf, __ } from '~/locale'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import { reportToSentry } from '../../utils'; import ActionComponent from '../jobs_shared/action_component.vue'; @@ -160,6 +160,21 @@ export default { hasAction() { return this.job.status && this.job.status.action && this.job.status.action.path; }, + hasUnauthorizedManualAction() { + return ( + !this.hasAction && + this.job.status?.group === 'manual' && + this.job.status?.label?.includes('(not allowed)') + ); + }, + unauthorizedManualActionIcon() { + /* + The action object is not available when the user cannot run the action. + So we can show the correct icon, extract the action name from the label instead: + "manual play action (not allowed)" or "manual stop action (not allowed)" + */ + return this.job.status?.label?.split(' ')[1]; + }, relatedDownstreamHovered() { return this.job.name === this.sourceJobHovered; }, @@ -198,6 +213,9 @@ export default { this.$emit('pipelineActionRequestComplete'); }, }, + i18n: { + unauthorizedTooltip: __('You are not authorized to run this manual job'), + }, }; </script> <template> @@ -242,8 +260,16 @@ export default { :link="status.action.path" :action-icon="status.action.icon" class="gl-mr-1" - data-qa-selector="action_button" + data-qa-selector="job_action_button" @pipelineActionRequestComplete="pipelineActionRequestComplete" /> + <action-component + v-if="hasUnauthorizedManualAction" + disabled + :tooltip-text="$options.i18n.unauthorizedTooltip" + :action-icon="unauthorizedManualActionIcon" + :link="`unauthorized-${computedJobId}`" + class="gl-mr-1" + /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index e0c1dcc5be5..c59f56fc68f 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui'; +import { GlBadge, GlButton, GlLink, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { __, sprintf } from '~/locale'; import CiStatus from '~/vue_shared/components/ci_icon.vue'; @@ -12,10 +12,10 @@ export default { }, components: { CiStatus, + GlBadge, GlButton, GlLink, GlLoadingIcon, - GlBadge, }, props: { columnTitle: { @@ -26,6 +26,10 @@ export default { type: Boolean, required: true, }, + isLoading: { + type: Boolean, + required: true, + }, pipeline: { type: Object, required: true, @@ -34,33 +38,40 @@ export default { type: String, required: true, }, - isLoading: { - type: Boolean, - required: true, - }, }, computed: { - tooltipText() { - return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} - - ${this.sourceJobInfo}`; + buttonBorderClass() { + return this.isUpstream ? 'gl-border-r-1!' : 'gl-border-l-1!'; }, buttonId() { return `js-linked-pipeline-${this.pipeline.id}`; }, - pipelineStatus() { - return this.pipeline.status; + cardSpacingClass() { + return this.isDownstream ? 'gl-pr-0' : ''; }, - projectName() { - return this.pipeline.project.name; + expandedIcon() { + if (this.isUpstream) { + return this.expanded ? 'angle-right' : 'angle-left'; + } + return this.expanded ? 'angle-left' : 'angle-right'; + }, + childPipeline() { + return this.isDownstream && this.isSameProject; }, downstreamTitle() { return this.childPipeline ? this.sourceJobName : this.pipeline.project.name; }, - parentPipeline() { - return this.isUpstream && this.isSameProject; + flexDirection() { + return this.isUpstream ? 'gl-flex-direction-row-reverse' : 'gl-flex-direction-row'; }, - childPipeline() { - return this.isDownstream && this.isSameProject; + isDownstream() { + return this.type === DOWNSTREAM; + }, + isSameProject() { + return !this.pipeline.multiproject; + }, + isUpstream() { + return this.type === UPSTREAM; }, label() { if (this.parentPipeline) { @@ -70,17 +81,17 @@ export default { } return __('Multi-project'); }, + parentPipeline() { + return this.isUpstream && this.isSameProject; + }, pipelineIsLoading() { return Boolean(this.isLoading || this.pipeline.isLoading); }, - isDownstream() { - return this.type === DOWNSTREAM; - }, - isUpstream() { - return this.type === UPSTREAM; + pipelineStatus() { + return this.pipeline.status; }, - isSameProject() { - return !this.pipeline.multiproject; + projectName() { + return this.pipeline.project.name; }, sourceJobName() { return this.pipeline.sourceJob?.name ?? ''; @@ -88,28 +99,23 @@ export default { sourceJobInfo() { return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : ''; }, - expandedIcon() { - if (this.isUpstream) { - return this.expanded ? 'angle-right' : 'angle-left'; - } - return this.expanded ? 'angle-left' : 'angle-right'; - }, - expandButtonPosition() { - return this.isUpstream ? 'gl-left-0 gl-border-r-1!' : 'gl-right-0 gl-border-l-1!'; + tooltipText() { + return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} - + ${this.sourceJobInfo}`; }, }, errorCaptured(err, _vm, info) { reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`); }, methods: { + hideTooltips() { + this.$root.$emit(BV_HIDE_TOOLTIP); + }, onClickLinkedPipeline() { this.hideTooltips(); this.$emit('pipelineClicked', this.$refs.linkedPipeline); this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded); }, - hideTooltips() { - this.$root.$emit(BV_HIDE_TOOLTIP); - }, onDownstreamHovered() { this.$emit('downstreamHovered', this.sourceJobName); }, @@ -124,27 +130,23 @@ export default { <div ref="linkedPipeline" v-gl-tooltip - class="gl-downstream-pipeline-job-width" + class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1" + :class="flexDirection" :title="tooltipText" data-qa-selector="child_pipeline" @mouseover="onDownstreamHovered" @mouseleave="onDownstreamHoverLeave" > - <div - class="gl-relative gl-bg-white gl-p-3 gl-border-solid gl-border-gray-100 gl-border-1" - :class="{ 'gl-pl-9': isUpstream }" - > - <div class="gl-display-flex gl-pr-7 gl-pipeline-job-width"> + <div class="gl-w-full gl-bg-white gl-p-3" :class="cardSpacingClass"> + <div class="gl-display-flex gl-pr-3"> <ci-status v-if="!pipelineIsLoading" :status="pipelineStatus" :size="24" css-classes="gl-top-0 gl-pr-2" /> - <div v-else class="gl-pr-2"><gl-loading-icon size="sm" inline /></div> - <div - class="gl-display-flex gl-flex-direction-column gl-pipeline-job-width gl-text-truncate" - > + <div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div> + <div class="gl-display-flex gl-flex-direction-column gl-downstream-pipeline-job-width"> <span class="gl-text-truncate" data-testid="downstream-title"> {{ downstreamTitle }} </span> @@ -160,10 +162,12 @@ export default { {{ label }} </gl-badge> </div> + </div> + <div class="gl-display-flex"> <gl-button :id="buttonId" - class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!" - :class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`" + class="gl-shadow-none! gl-rounded-0!" + :class="`js-pipeline-expand-${pipeline.id} ${buttonBorderClass}`" :icon="expandedIcon" :aria-label="__('Expand pipeline')" data-testid="expand-pipeline-button" diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 8088858f381..6a4d1bb44f2 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -1,9 +1,22 @@ <script> -import { GlAlert, GlButton, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui'; +import { + GlAlert, + GlButton, + GlLoadingIcon, + GlModal, + GlModalDirective, + GlTooltipDirective, +} from '@gitlab/ui'; import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import ciHeader from '~/vue_shared/components/header_ci_component.vue'; -import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants'; +import { + LOAD_FAILURE, + POST_FAILURE, + DELETE_FAILURE, + DEFAULT, + BUTTON_TOOLTIP_RETRY, +} from '../constants'; import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutation.graphql'; import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutation.graphql'; import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql'; @@ -15,6 +28,7 @@ const POLL_INTERVAL = 10000; export default { name: 'PipelineHeaderSection', + BUTTON_TOOLTIP_RETRY, pipelineCancel: 'pipelineCancel', pipelineRetry: 'pipelineRetry', finishedStatuses: ['FAILED', 'SUCCESS', 'CANCELED'], @@ -27,6 +41,7 @@ export default { }, directives: { GlModal: GlModalDirective, + GlTooltip: GlTooltipDirective, }, errorTexts: { [LOAD_FAILURE]: __('We are currently unable to fetch data for the pipeline header.'), @@ -225,6 +240,9 @@ export default { > <gl-button v-if="canRetryPipeline" + v-gl-tooltip + :aria-label="$options.BUTTON_TOOLTIP_RETRY" + :title="$options.BUTTON_TOOLTIP_RETRY" :loading="isRetrying" :disabled="isRetrying" category="secondary" diff --git a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue index e11073aee33..99fb5c146ba 100644 --- a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue +++ b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue @@ -36,10 +36,13 @@ export default { return data.project?.pipeline?.jobs?.nodes || []; }, result({ data }) { + if (!data) { + return; + } this.jobsPageInfo = data.project?.pipeline?.jobs?.pageInfo || {}; }, error() { - createFlash({ message: __('An error occured while fetching the pipelines jobs.') }); + createFlash({ message: __('An error occurred while fetching the pipelines jobs.') }); }, }, }, diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue index efad43ddd4f..ca2537ca4f4 100644 --- a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue +++ b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue @@ -92,14 +92,20 @@ export default { <template> <gl-button :id="`js-ci-action-${link}`" - v-gl-tooltip="{ boundary: 'viewport' }" - :title="tooltipText" :class="cssClass" :disabled="isDisabled" class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center" + data-testid="ci-action-component" @click.stop="onClickAction" > - <gl-loading-icon v-if="isLoading" size="sm" class="js-action-icon-loading" /> - <gl-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" /> + <div + v-gl-tooltip.viewport + :title="tooltipText" + class="gl-display-flex gl-align-items-center gl-justify-content-center gl-h-full" + data-testid="ci-action-icon-tooltip-wrapper" + > + <gl-loading-icon v-if="isLoading" size="sm" class="js-action-icon-loading" /> + <gl-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" /> + </div> </gl-button> </template> diff --git a/app/assets/javascripts/pipelines/components/notification/deprecated_type_keyword_notification.vue b/app/assets/javascripts/pipelines/components/notification/deprecated_type_keyword_notification.vue new file mode 100644 index 00000000000..b8f9f84c217 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/notification/deprecated_type_keyword_notification.vue @@ -0,0 +1,102 @@ +<script> +import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; +import { __ } from '~/locale'; +import getPipelineWarnings from '../../graphql/queries/get_pipeline_warnings.query.graphql'; + +export default { + // eslint-disable-next-line @gitlab/require-i18n-strings + expectedMessage: 'will be removed in', + i18n: { + title: __('Found warning in your .gitlab-ci.yml'), + rootTypesWarning: __( + '%{codeStart}types%{codeEnd} is deprecated and will be removed in 15.0. Use %{codeStart}stages%{codeEnd} instead. %{linkStart}Learn More %{linkEnd}', + ), + typeWarning: __( + '%{codeStart}type%{codeEnd} is deprecated and will be removed in 15.0. Use %{codeStart}stage%{codeEnd} instead. %{linkStart}Learn More %{linkEnd}', + ), + }, + components: { + GlAlert, + GlLink, + GlSprintf, + }, + inject: ['deprecatedKeywordsDocPath', 'fullPath', 'pipelineIid'], + apollo: { + warnings: { + query: getPipelineWarnings, + variables() { + return { + fullPath: this.fullPath, + iid: this.pipelineIid, + }; + }, + update(data) { + return data?.project?.pipeline?.warningMessages || []; + }, + error() { + this.hasError = true; + }, + }, + }, + data() { + return { + warnings: [], + hasError: false, + }; + }, + computed: { + deprecationWarnings() { + return this.warnings.filter(({ content }) => { + return content.includes(this.$options.expectedMessage); + }); + }, + formattedWarnings() { + // The API doesn't have a mechanism currently to return a + // type instead of just the error message. To work around this, + // we check if the deprecation message is found within the warnings + // and show a FE version of that message with the link to the documentation + // and translated. We can have only 2 types of warnings: root types and individual + // type. If the word `root` is present, then we know it's the root type deprecation + // and if not, it's the normal type. This has to be deleted in 15.0. + // Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/350810 + return this.deprecationWarnings.map(({ content }) => { + if (content.includes('root')) { + return this.$options.i18n.rootTypesWarning; + } + return this.$options.i18n.typeWarning; + }); + }, + hasDeprecationWarning() { + return this.formattedWarnings.length > 0; + }, + showWarning() { + return ( + !this.$apollo.queries.warnings?.loading && !this.hasError && this.hasDeprecationWarning + ); + }, + }, +}; +</script> +<template> + <div> + <gl-alert + v-if="showWarning" + :title="$options.i18n.title" + variant="warning" + :dismissible="false" + > + <ul class="gl-mb-0"> + <li v-for="warning in formattedWarnings" :key="warning"> + <gl-sprintf :message="warning"> + <template #code="{ content }"> + <code> {{ content }}</code> + </template> + <template #link="{ content }"> + <gl-link :href="deprecatedKeywordsDocPath" target="_blank"> {{ content }}</gl-link> + </template> + </gl-sprintf> + </li> + </ul> + </gl-alert> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue index b6c178d20b0..fa0e153b2af 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue @@ -1,15 +1,13 @@ <script> import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; -import { __ } from '~/locale'; import eventHub from '../../event_hub'; +import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '../../constants'; import PipelineMultiActions from './pipeline_multi_actions.vue'; import PipelinesManualActions from './pipelines_manual_actions.vue'; export default { - i18n: { - cancelTitle: __('Cancel'), - redeployTitle: __('Retry'), - }, + BUTTON_TOOLTIP_RETRY, + BUTTON_TOOLTIP_CANCEL, directives: { GlTooltip: GlTooltipDirective, GlModalDirective, @@ -75,12 +73,13 @@ export default { <gl-button v-if="pipeline.flags.retryable" v-gl-tooltip.hover - :aria-label="$options.i18n.redeployTitle" - :title="$options.i18n.redeployTitle" + :aria-label="$options.BUTTON_TOOLTIP_RETRY" + :title="$options.BUTTON_TOOLTIP_RETRY" :disabled="isRetrying" :loading="isRetrying" class="js-pipelines-retry-button" data-qa-selector="pipeline_retry_button" + data-testid="pipelines-retry-button" icon="repeat" variant="default" category="secondary" @@ -91,14 +90,15 @@ export default { v-if="pipeline.flags.cancelable" v-gl-tooltip.hover v-gl-modal-directive="'confirmation-modal'" - :aria-label="$options.i18n.cancelTitle" - :title="$options.i18n.cancelTitle" + :aria-label="$options.BUTTON_TOOLTIP_CANCEL" + :title="$options.BUTTON_TOOLTIP_CANCEL" :loading="isCancelling" :disabled="isCancelling" icon="cancel" variant="danger" category="primary" class="js-pipelines-cancel-button gl-ml-1" + data-testid="pipelines-cancel-button" @click="handleCancelClick" /> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue index 0528e4c147c..b29c8426301 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue @@ -26,7 +26,7 @@ export default { v-if="user" :link-href="user.path" :img-src="user.avatar_url" - :img-size="26" + :img-size="32" :tooltip-text="user.name" class="gl-ml-3 js-pipeline-url-user" /> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue index e2f30d5a8e6..52da4d01468 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -1,15 +1,19 @@ <script> -import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui'; +import { GlIcon, GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; import { helpPagePath } from '~/helpers/help_page_helper'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { SCHEDULE_ORIGIN } from '../../constants'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; +import { SCHEDULE_ORIGIN, ICONS } from '../../constants'; export default { components: { + GlIcon, GlLink, GlPopover, GlSprintf, GlBadge, + TooltipOnTruncate, }, directives: { GlTooltip: GlTooltipDirective, @@ -33,11 +37,12 @@ export default { type: String, required: true, }, + viewType: { + type: String, + required: true, + }, }, computed: { - user() { - return this.pipeline.user; - }, isScheduled() { return this.pipeline.source === SCHEDULE_ORIGIN; }, @@ -53,12 +58,160 @@ export default { autoDevopsHelpPath() { return helpPagePath('topics/autodevops/index.md'); }, + mergeRequestRef() { + return this.pipeline?.merge_request; + }, + commitRef() { + return this.pipeline?.ref; + }, + commitTag() { + return this.commitRef?.tag; + }, + commitUrl() { + return this.pipeline?.commit?.commit_path; + }, + commitShortSha() { + return this.pipeline?.commit?.short_id; + }, + refUrl() { + return this.commitRef?.ref_url || this.commitRef?.path; + }, + tooltipTitle() { + return this.mergeRequestRef?.title || this.commitRef?.name; + }, + commitAuthor() { + let commitAuthorInformation; + const pipelineCommit = this.pipeline?.commit; + const pipelineCommitAuthor = pipelineCommit?.author; + + if (!pipelineCommit) { + return null; + } + + // 1. person who is an author of a commit might be a GitLab user + if (pipelineCommitAuthor) { + // 2. if person who is an author of a commit is a GitLab user + // they can have a GitLab avatar + if (pipelineCommitAuthor?.avatar_url) { + commitAuthorInformation = pipelineCommitAuthor; + + // 3. If GitLab user does not have avatar, they might have a Gravatar + } else if (pipelineCommit.author_gravatar_url) { + commitAuthorInformation = { + ...pipelineCommitAuthor, + avatar_url: pipelineCommit.author_gravatar_url, + }; + } + // 4. If committer is not a GitLab User, they can have a Gravatar + } else { + commitAuthorInformation = { + avatar_url: pipelineCommit.author_gravatar_url, + path: `mailto:${pipelineCommit.author_email}`, + username: pipelineCommit.author_name, + }; + } + + return commitAuthorInformation; + }, + commitIcon() { + let name = ''; + + if (this.commitTag) { + name = ICONS.TAG; + } else if (this.mergeRequestRef) { + name = ICONS.MR; + } else { + name = ICONS.BRANCH; + } + + return name; + }, + commitIconTooltipTitle() { + switch (this.commitIcon) { + case ICONS.TAG: + return __('Tag'); + case ICONS.MR: + return __('Merge Request'); + default: + return __('Branch'); + } + }, + commitTitleText() { + return this.pipeline?.commit?.title || __("Can't find HEAD commit for this branch"); + }, + hasAuthor() { + return ( + this.commitAuthor?.avatar_url && this.commitAuthor?.path && this.commitAuthor?.username + ); + }, + userImageAltDescription() { + return this.commitAuthor?.username + ? sprintf(__("%{username}'s avatar"), { username: this.commitAuthor.username }) + : null; + }, + rearrangePipelinesTable() { + return this.glFeatures?.rearrangePipelinesTable; + }, }, }; </script> <template> <div class="pipeline-tags" data-testid="pipeline-url-table-cell"> + <template v-if="rearrangePipelinesTable"> + <div class="commit-title gl-mb-2" data-testid="commit-title-container"> + <span class="gl-display-flex"> + <tooltip-on-truncate :title="commitTitleText" class="flex-truncate-child gl-flex-grow-1"> + <gl-link + :href="pipeline.path" + class="commit-row-message gl-text-blue-600!" + data-testid="commit-title" + data-qa-selector="pipeline_url_link" + >{{ commitTitleText }}</gl-link + > + </tooltip-on-truncate> + </span> + </div> + <div class="gl-mb-2"> + <span class="gl-font-weight-bold gl-text-gray-500" data-testid="pipeline-identifier"> + #{{ pipeline[pipelineKey] }} + </span> + <!--Commit row--> + <div class="icon-container gl-display-inline-block"> + <gl-icon + v-gl-tooltip + :name="commitIcon" + :title="commitIconTooltipTitle" + data-testid="commit-icon-type" + /> + </div> + <tooltip-on-truncate :title="tooltipTitle" truncate-target="child" placement="top"> + <gl-link + v-if="mergeRequestRef" + :href="mergeRequestRef.path" + class="ref-name" + data-testid="merge-request-ref" + >{{ mergeRequestRef.iid }}</gl-link + > + <gl-link v-else :href="refUrl" class="ref-name" data-testid="commit-ref-name">{{ + commitRef.name + }}</gl-link> + </tooltip-on-truncate> + <gl-icon + v-gl-tooltip + name="commit" + class="commit-icon" + :title="__('Commit')" + data-testid="commit-icon" + /> + + <gl-link :href="commitUrl" class="commit-sha mr-0" data-testid="commit-short-sha">{{ + commitShortSha + }}</gl-link> + <!--End of commit row--> + </div> + </template> <gl-link + v-if="!rearrangePipelinesTable" :href="pipeline.path" class="gl-text-decoration-underline" data-testid="pipeline-url-link" @@ -66,7 +219,7 @@ export default { > #{{ pipeline[pipelineKey] }} </gl-link> - <div class="label-container"> + <div class="label-container gl-mt-1"> <gl-badge v-if="isScheduled" v-gl-tooltip @@ -163,7 +316,7 @@ export default { v-gl-tooltip :title=" __( - 'Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.', + 'Merge request pipelines are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for merge request pipelines.', ) " variant="info" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue index b94f1a42039..47fffa8a6b2 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue @@ -2,6 +2,7 @@ import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import { s__, __, sprintf } from '~/locale'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; import eventHub from '../../event_hub'; @@ -28,7 +29,7 @@ export default { }; }, methods: { - onClickAction(action) { + async onClickAction(action) { if (action.scheduled_at) { const confirmationMessage = sprintf( s__( @@ -36,9 +37,10 @@ export default { ), { jobName: action.name }, ); - // https://gitlab.com/gitlab-org/gitlab-foss/issues/52156 - // eslint-disable-next-line no-alert - if (!window.confirm(confirmationMessage)) { + + const confirmed = await confirmAction(confirmationMessage); + + if (!confirmed) { return; } } diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue index f56457a4162..54901c2d13f 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue @@ -3,12 +3,16 @@ import CodeQualityWalkthrough from '~/code_quality_walkthrough/components/step.v import { PIPELINE_STATUSES } from '~/code_quality_walkthrough/constants'; import { CHILD_VIEW } from '~/pipelines/constants'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import PipelinesTimeago from './time_ago.vue'; export default { components: { CodeQualityWalkthrough, CiBadge, + PipelinesTimeago, }, + mixins: [glFeatureFlagsMixin()], props: { pipeline: { type: Object, @@ -40,6 +44,9 @@ export default { codeQualityBuildPath() { return this.pipeline?.details?.code_quality_build_path; }, + rearrangePipelinesTable() { + return this.glFeatures?.rearrangePipelinesTable; + }, }, }; </script> @@ -48,11 +55,13 @@ export default { <div> <ci-badge id="js-code-quality-walkthrough" + class="gl-mb-3" :status="pipelineStatus" :show-text="!isChildView" :icon-classes="'gl-vertical-align-middle!'" data-qa-selector="pipeline_commit_status" /> + <pipelines-timeago v-if="rearrangePipelinesTable" class="gl-mt-3" :pipeline="pipeline" /> <code-quality-walkthrough v-if="shouldRenderCodeQualityWalkthrough" :step="codeQualityStep" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index d64decc81ec..9919a18cb99 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -1,6 +1,7 @@ <script> import { GlTableLite, GlTooltipDirective } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { s__, __ } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import eventHub from '../../event_hub'; import PipelineMiniGraph from './pipeline_mini_graph.vue'; import PipelineOperations from './pipeline_operations.vue'; @@ -33,6 +34,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + mixins: [glFeatureFlagMixin()], props: { pipelines: { type: Array, @@ -72,16 +74,18 @@ export default { key: 'status', label: s__('Pipeline|Status'), thClass: DEFAULT_TH_CLASSES, - columnClass: 'gl-w-10p', + columnClass: this.rearrangePipelinesTable ? 'gl-w-15p' : 'gl-w-10p', tdClass: DEFAULT_TD_CLASS, thAttr: { 'data-testid': 'status-th' }, }, { key: 'pipeline', - label: this.pipelineKeyOption.label, + label: this.rearrangePipelinesTable ? __('Pipeline') : this.pipelineKeyOption.label, thClass: DEFAULT_TH_CLASSES, - tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`, - columnClass: 'gl-w-10p', + tdClass: this.rearrangePipelinesTable + ? `${DEFAULT_TD_CLASS}` + : `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`, + columnClass: this.rearrangePipelinesTable ? 'gl-w-30p' : 'gl-w-10p', thAttr: { 'data-testid': 'pipeline-th' }, }, { @@ -113,7 +117,7 @@ export default { label: s__('Pipeline|Duration'), thClass: DEFAULT_TH_CLASSES, tdClass: DEFAULT_TD_CLASS, - columnClass: 'gl-w-15p', + columnClass: this.rearrangePipelinesTable ? 'gl-w-5p' : 'gl-w-15p', thAttr: { 'data-testid': 'timeago-th' }, }, { @@ -124,7 +128,13 @@ export default { thAttr: { 'data-testid': 'actions-th' }, }, ]; - return fields; + + return !this.rearrangePipelinesTable + ? fields + : fields.filter((field) => !['commit', 'timeago'].includes(field.key)); + }, + rearrangePipelinesTable() { + return this.glFeatures?.rearrangePipelinesTable; }, }, watch: { @@ -182,6 +192,7 @@ export default { :pipeline="item" :pipeline-schedule-url="pipelineScheduleUrl" :pipeline-key="pipelineKeyOption.key" + :view-type="viewType" /> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue index e6b03751350..c45e3f24567 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue @@ -54,11 +54,14 @@ export default { showSkipped() { return !this.duration && !this.finishedTime && this.skipped; }, + shouldDisplayAsBlock() { + return this.glFeatures?.rearrangePipelinesTable; + }, }, }; </script> <template> - <div> + <div class="{ 'gl-display-block': shouldDisplayAsBlock }"> <span v-if="showInProgress" data-testid="pipeline-in-progress"> <gl-icon v-if="stuck" name="warning" class="gl-mr-2" :size="12" data-testid="warning-icon" /> <gl-icon @@ -87,6 +90,7 @@ export default { <time v-gl-tooltip :title="tooltipTitle(finishedTime)" + :datetime="finishedTime" data-placement="top" data-container="body" > diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index 410fc7b82cd..36f708ff2af 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -10,6 +10,12 @@ export const SCHEDULE_ORIGIN = 'schedule'; export const NEEDS_PROPERTY = 'needs'; export const EXPLICIT_NEEDS_PROPERTY = 'previousStageJobsOrNeeds'; +export const ICONS = { + TAG: 'tag', + MR: 'git-merge', + BRANCH: 'branch', +}; + export const TestStatus = { FAILED: 'failed', SKIPPED: 'skipped', @@ -53,3 +59,6 @@ export const PipelineKeyOptions = [ ]; export const TOAST_MESSAGE = s__('Pipeline|Creating pipeline.'); + +export const BUTTON_TOOLTIP_RETRY = __('Retry failed jobs'); +export const BUTTON_TOOLTIP_CANCEL = __('Cancel'); diff --git a/app/assets/javascripts/pipelines/graphql/fragmentTypes.json b/app/assets/javascripts/pipelines/graphql/fragmentTypes.json deleted file mode 100644 index 4601b74b5c1..00000000000 --- a/app/assets/javascripts/pipelines/graphql/fragmentTypes.json +++ /dev/null @@ -1 +0,0 @@ -{"__schema":{"types":[{"kind":"UNION","name":"JobNeedUnion","possibleTypes":[{"name":"CiBuildNeed"},{"name":"CiJob"}]}]}}
\ No newline at end of file diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_warnings.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_warnings.query.graphql new file mode 100644 index 00000000000..cd1d2b62a3d --- /dev/null +++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_warnings.query.graphql @@ -0,0 +1,12 @@ +query getPipelineWarnings($fullPath: ID!, $iid: ID!) { + project(fullPath: $fullPath) { + id + pipeline(iid: $iid) { + id + warningMessages { + content + id + } + } + } +} diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js index 3201f88a9e3..c4f7665c91d 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js @@ -1,6 +1,7 @@ import Visibility from 'visibilityjs'; import createFlash from '~/flash'; import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; +import httpStatusCodes from '~/lib/utils/http_status'; import Poll from '~/lib/utils/poll'; import { __ } from '~/locale'; import { validateParams } from '~/pipelines/utils'; @@ -195,11 +196,20 @@ export default { this.$toast.show(TOAST_MESSAGE); this.updateTable(); }) - .catch(() => { + .catch((e) => { + const unauthorized = e.response.status === httpStatusCodes.UNAUTHORIZED; + const badRequest = e.response.status === httpStatusCodes.BAD_REQUEST; + + let errorMessage = __( + 'An error occurred while trying to run a new pipeline for this merge request.', + ); + + if (unauthorized || badRequest) { + errorMessage = __('You do not have permission to run a pipeline on this branch.'); + } + createFlash({ - message: __( - 'An error occurred while trying to run a new pipeline for this merge request.', - ), + message: errorMessage, }); }) .finally(() => this.store.toggleIsRunningPipeline(false)); diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index ae8b2503c79..bfb95e5ab0c 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -3,6 +3,7 @@ import { __ } from '~/locale'; import createDagApp from './pipeline_details_dag'; import { createPipelinesDetailApp } from './pipeline_details_graph'; import { createPipelineHeaderApp } from './pipeline_details_header'; +import { createPipelineNotificationApp } from './pipeline_details_notification'; import { createPipelineJobsApp } from './pipeline_details_jobs'; import { apolloProvider } from './pipeline_shared_client'; import { createTestDetails } from './pipeline_test_details'; @@ -11,6 +12,7 @@ const SELECTORS = { PIPELINE_DETAILS: '.js-pipeline-details-vue', PIPELINE_GRAPH: '#js-pipeline-graph-vue', PIPELINE_HEADER: '#js-pipeline-header-vue', + PIPELINE_NOTIFICATION: '#js-pipeline-notification', PIPELINE_TESTS: '#js-pipeline-tests-detail', PIPELINE_JOBS: '#js-pipeline-jobs-vue', }; @@ -43,6 +45,14 @@ export default async function initPipelineDetailsBundle() { } try { + createPipelineNotificationApp(SELECTORS.PIPELINE_NOTIFICATION, apolloProvider); + } catch { + createFlash({ + message: __('An error occurred while loading a section of this page.'), + }); + } + + try { createDagApp(apolloProvider); } catch { createFlash({ diff --git a/app/assets/javascripts/pipelines/pipeline_details_notification.js b/app/assets/javascripts/pipelines/pipeline_details_notification.js new file mode 100644 index 00000000000..0061be843c5 --- /dev/null +++ b/app/assets/javascripts/pipelines/pipeline_details_notification.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import DeprecatedKeywordNotification from './components/notification/deprecated_type_keyword_notification.vue'; + +Vue.use(VueApollo); + +export const createPipelineNotificationApp = (elSelector, apolloProvider) => { + const el = document.querySelector(elSelector); + + if (!el) { + return; + } + + const { deprecatedKeywordsDocPath, fullPath, pipelineIid } = el?.dataset; + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + DeprecatedKeywordNotification, + }, + provide: { + deprecatedKeywordsDocPath, + fullPath, + pipelineIid, + }, + apolloProvider, + render(createElement) { + return createElement('deprecated-keyword-notification'); + }, + }); +}; diff --git a/app/assets/javascripts/pipelines/pipeline_shared_client.js b/app/assets/javascripts/pipelines/pipeline_shared_client.js index 84276588d6a..c3be487caae 100644 --- a/app/assets/javascripts/pipelines/pipeline_shared_client.js +++ b/app/assets/javascripts/pipelines/pipeline_shared_client.js @@ -1,19 +1,10 @@ import VueApollo from 'vue-apollo'; -import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; import createDefaultClient from '~/lib/graphql'; -import introspectionQueryResultData from './graphql/fragmentTypes.json'; - -export const fragmentMatcher = new IntrospectionFragmentMatcher({ - introspectionQueryResultData, -}); export const apolloProvider = new VueApollo({ defaultClient: createDefaultClient( {}, { - cacheConfig: { - fragmentMatcher, - }, useGet: true, }, ), |