summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_merge_request_widget/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget/components')
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue212
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue70
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue50
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/loading.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue72
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue109
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue121
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_terraform_plan.vue139
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue136
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue31
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue140
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue111
24 files changed, 886 insertions, 384 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
new file mode 100644
index 00000000000..0f9d1b8395b
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -0,0 +1,212 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import eventHub from '../../event_hub';
+import approvalsMixin from '../../mixins/approvals';
+import MrWidgetContainer from '../mr_widget_container.vue';
+import MrWidgetIcon from '../mr_widget_icon.vue';
+import ApprovalsSummary from './approvals_summary.vue';
+import ApprovalsSummaryOptional from './approvals_summary_optional.vue';
+import { FETCH_LOADING, FETCH_ERROR, APPROVE_ERROR, UNAPPROVE_ERROR } from './messages';
+
+export default {
+ name: 'MRWidgetApprovals',
+ components: {
+ MrWidgetContainer,
+ MrWidgetIcon,
+ ApprovalsSummary,
+ ApprovalsSummaryOptional,
+ GlButton,
+ },
+ mixins: [approvalsMixin],
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ service: {
+ type: Object,
+ required: true,
+ },
+ isOptionalDefault: {
+ type: Boolean,
+ required: false,
+ default: null,
+ },
+ approveDefault: {
+ type: Function,
+ required: false,
+ default: null,
+ },
+ modalId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ requirePasswordToApprove: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ fetchingApprovals: true,
+ hasApprovalAuthError: false,
+ isApproving: false,
+ };
+ },
+ computed: {
+ isBasic() {
+ return this.mr.approvalsWidgetType === 'base';
+ },
+ isApproved() {
+ return Boolean(this.approvals.approved);
+ },
+ isOptional() {
+ return this.isOptionalDefault !== null ? this.isOptionalDefault : !this.approvedBy.length;
+ },
+ hasAction() {
+ return Boolean(this.action);
+ },
+ approvals() {
+ return this.mr.approvals || {};
+ },
+ approvedBy() {
+ return this.approvals.approved_by ? this.approvals.approved_by.map(x => x.user) : [];
+ },
+ userHasApproved() {
+ return Boolean(this.approvals.user_has_approved);
+ },
+ userCanApprove() {
+ return Boolean(this.approvals.user_can_approve);
+ },
+ showApprove() {
+ return !this.userHasApproved && this.userCanApprove && this.mr.isOpen;
+ },
+ showUnapprove() {
+ return this.userHasApproved && !this.userCanApprove && this.mr.state !== 'merged';
+ },
+ approvalText() {
+ return this.isApproved && this.approvedBy.length > 0
+ ? s__('mrWidget|Approve additionally')
+ : s__('mrWidget|Approve');
+ },
+ action() {
+ // Use the default approve action, only if we aren't using the auth component for it
+ if (this.showApprove) {
+ return {
+ text: this.approvalText,
+ category: this.isApproved ? 'secondary' : 'primary',
+ variant: 'info',
+ action: () => this.approve(),
+ };
+ } else if (this.showUnapprove) {
+ return {
+ text: s__('mrWidget|Revoke approval'),
+ variant: 'warning',
+ category: 'secondary',
+ action: () => this.unapprove(),
+ };
+ }
+
+ return null;
+ },
+ },
+ created() {
+ this.refreshApprovals()
+ .then(() => {
+ this.fetchingApprovals = false;
+ })
+ .catch(() => createFlash(FETCH_ERROR));
+ },
+ methods: {
+ approve() {
+ if (this.requirePasswordToApprove) {
+ this.$root.$emit('bv::show::modal', this.modalId);
+ return;
+ }
+
+ this.updateApproval(
+ () => this.service.approveMergeRequest(),
+ () => createFlash(APPROVE_ERROR),
+ );
+ },
+ approveWithAuth(data) {
+ this.updateApproval(
+ () => this.service.approveMergeRequestWithAuth(data),
+ error => {
+ if (error && error.response && error.response.status === 401) {
+ this.hasApprovalAuthError = true;
+ return;
+ }
+ createFlash(APPROVE_ERROR);
+ },
+ );
+ },
+ unapprove() {
+ this.updateApproval(
+ () => this.service.unapproveMergeRequest(),
+ () => createFlash(UNAPPROVE_ERROR),
+ );
+ },
+ updateApproval(serviceFn, errFn) {
+ this.isApproving = true;
+ this.clearError();
+ return serviceFn()
+ .then(data => {
+ this.mr.setApprovals(data);
+ eventHub.$emit('MRWidgetUpdateRequested');
+ this.$emit('updated');
+ })
+ .catch(errFn)
+ .then(() => {
+ this.isApproving = false;
+ });
+ },
+ },
+ FETCH_LOADING,
+};
+</script>
+<template>
+ <mr-widget-container>
+ <div class="js-mr-approvals d-flex align-items-start align-items-md-center">
+ <mr-widget-icon name="approval" />
+ <div v-if="fetchingApprovals">{{ $options.FETCH_LOADING }}</div>
+ <template v-else>
+ <gl-button
+ v-if="action"
+ :variant="action.variant"
+ :category="action.category"
+ :loading="isApproving"
+ class="mr-3"
+ data-qa-selector="approve_button"
+ @click="action.action"
+ >
+ {{ action.text }}
+ </gl-button>
+ <approvals-summary-optional
+ v-if="isOptional"
+ :can-approve="hasAction"
+ :help-path="mr.approvalsHelpPath"
+ />
+ <approvals-summary
+ v-else
+ :approved="isApproved"
+ :approvals-left="approvals.approvals_left || 0"
+ :rules-left="approvals.approvalRuleNamesLeft"
+ :approvers="approvedBy"
+ />
+ <slot
+ :is-approving="isApproving"
+ :approve-with-auth="approveWithAuth"
+ :hasApproval-auth-error="hasApprovalAuthError"
+ ></slot>
+ </template>
+ </div>
+ <template #footer>
+ <slot name="footer"></slot>
+ </template>
+ </mr-widget-container>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
new file mode 100644
index 00000000000..fb342a5d340
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
@@ -0,0 +1,70 @@
+<script>
+import { n__, sprintf } from '~/locale';
+import { toNounSeriesText } from '~/lib/utils/grammar';
+import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
+import { APPROVED_MESSAGE } from '~/vue_merge_request_widget/components/approvals/messages';
+
+export default {
+ components: {
+ UserAvatarList,
+ },
+ props: {
+ approved: {
+ type: Boolean,
+ required: true,
+ },
+ approvalsLeft: {
+ type: Number,
+ required: true,
+ },
+ rulesLeft: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ approvers: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ message() {
+ if (this.approved) {
+ return APPROVED_MESSAGE;
+ }
+
+ if (!this.rulesLeft.length) {
+ return n__('Requires approval.', 'Requires %d more approvals.', this.approvalsLeft);
+ }
+
+ return sprintf(
+ n__(
+ 'Requires approval from %{names}.',
+ 'Requires %{count} more approvals from %{names}.',
+ this.approvalsLeft,
+ ),
+ {
+ names: toNounSeriesText(this.rulesLeft),
+ count: this.approvalsLeft,
+ },
+ false,
+ );
+ },
+ hasApprovers() {
+ return Boolean(this.approvers.length);
+ },
+ },
+ APPROVED_MESSAGE,
+};
+</script>
+
+<template>
+ <div data-qa-selector="approvals_summary_content">
+ <strong>{{ message }}</strong>
+ <template v-if="hasApprovers">
+ <span>{{ s__('mrWidget|Approved by') }}</span>
+ <user-avatar-list class="d-inline-block align-middle" :items="approvers" />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue
new file mode 100644
index 00000000000..66af0c5a83e
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import {
+ OPTIONAL,
+ OPTIONAL_CAN_APPROVE,
+} from '~/vue_merge_request_widget/components/approvals/messages';
+
+export default {
+ components: {
+ GlLink,
+ Icon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ canApprove: {
+ type: Boolean,
+ required: true,
+ },
+ helpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ message() {
+ return this.canApprove ? OPTIONAL_CAN_APPROVE : OPTIONAL;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="d-flex align-items-center">
+ <span class="text-muted">{{ message }}</span>
+ <gl-link
+ v-if="canApprove && helpPath"
+ v-gl-tooltip
+ :href="helpPath"
+ :title="__('About this feature')"
+ target="_blank"
+ class="d-flex-center pl-1"
+ >
+ <icon name="question" />
+ </gl-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js b/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js
new file mode 100644
index 00000000000..1d9368f71aa
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js
@@ -0,0 +1,11 @@
+import { __, s__ } from '~/locale';
+
+export const FETCH_LOADING = __('Checking approval status');
+export const FETCH_ERROR = s__(
+ 'mrWidget|An error occurred while retrieving approval data for this merge request.',
+);
+export const APPROVE_ERROR = s__('mrWidget|An error occurred while submitting your approval.');
+export const UNAPPROVE_ERROR = s__('mrWidget|An error occurred while removing your approval.');
+export const APPROVED_MESSAGE = s__('mrWidget|Merge request approved.');
+export const OPTIONAL_CAN_APPROVE = s__('mrWidget|No approval required; you can still approve');
+export const OPTIONAL = s__('mrWidget|No approval required');
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/loading.vue b/app/assets/javascripts/vue_merge_request_widget/components/loading.vue
index 78dc28ee92b..cd4e31e0dae 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/loading.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/loading.vue
@@ -9,7 +9,7 @@ export default {
</script>
<template>
- <div class="prepend-top-default">
+ <div class="gl-mt-3">
<div class="mr-widget-heading p-3">
<gl-skeleton-loader :width="577" :height="12">
<rect width="86" height="12" rx="2" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
index 294871ca5c2..24174c29d51 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
@@ -1,11 +1,11 @@
<script>
-import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
GlLoadingIcon,
Icon,
},
@@ -58,16 +58,17 @@ export default {
</div>
<template v-else>
- <gl-deprecated-button
- class="btn-blank btn s32 square append-right-default"
+ <button
+ class="btn-blank btn s32 square gl-mr-3"
+ type="button"
:aria-label="ariaLabel"
:disabled="isLoading"
@click="toggleCollapsed"
>
<gl-loading-icon v-if="isLoading" />
<icon v-else :name="arrowIconName" class="js-icon" />
- </gl-deprecated-button>
- <gl-deprecated-button
+ </button>
+ <gl-button
variant="link"
class="js-title"
:disabled="isLoading"
@@ -76,7 +77,7 @@ export default {
>
<template v-if="isCollapsed">{{ title }}</template>
<template v-else>{{ __('Collapse') }}</template>
- </gl-deprecated-button>
+ </gl-button>
</template>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
index 84937aa9510..598b08f4c16 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
@@ -27,7 +27,7 @@ export default {
return this.author.webUrl || this.author.web_url;
},
avatarUrl() {
- return this.author.avatarUrl || this.author.avatar_url;
+ return this.author.avatarUrl || this.author.avatar_url || gl.mrWidgetData.defaultAvatarUrl;
},
},
};
@@ -40,6 +40,6 @@ export default {
class="author-link inline"
>
<img :src="avatarUrl" class="avatar avatar-inline s16" />
- <span v-if="showAuthorName" class="author"> {{ author.name }} </span>
+ <span v-if="showAuthorName" class="author">{{ author.name }}</span>
</a>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue
new file mode 100644
index 00000000000..fd999540f4a
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue
@@ -0,0 +1,72 @@
+<script>
+import { __ } from '~/locale';
+import { GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
+
+/**
+ * Renders header section with icon and expand button
+ * Renders expanable content section with grey background
+ */
+export default {
+ name: 'MrWidgetExpanableSection',
+ components: {
+ GlButton,
+ GlCollapse,
+ GlIcon,
+ },
+ props: {
+ iconName: {
+ type: String,
+ required: false,
+ default: 'status_warning',
+ },
+ },
+ data() {
+ return {
+ contentIsVisible: false,
+ };
+ },
+ computed: {
+ collapseButtonText() {
+ if (this.contentIsVisible) {
+ return __('Collapse');
+ }
+
+ return __('Expand');
+ },
+ },
+ methods: {
+ updateContentVisibility() {
+ this.contentIsVisible = !this.contentIsVisible;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="mr-widget-body gl-display-flex">
+ <span
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-mr-3 gl-align-self-start gl-mt-1"
+ >
+ <gl-icon :name="iconName" :size="24" />
+ </span>
+
+ <div class="gl-display-flex gl-flex-fill-1 gl-flex-direction-column gl-md-flex-direction-row">
+ <slot name="header"></slot>
+
+ <div>
+ <gl-button @click="updateContentVisibility">
+ {{ collapseButtonText }}
+ </gl-button>
+ </div>
+ </div>
+ </div>
+
+ <gl-collapse
+ :visible="contentIsVisible"
+ class="gl-bg-gray-10 gl-border-t-solid gl-border-gray-100 gl-border-1"
+ >
+ <slot name="content"></slot>
+ </gl-collapse>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 0464c4b9c15..897f706290d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,4 +1,5 @@
<script>
+import Mousetrap from 'mousetrap';
import { escape } from 'lodash';
import { n__, s__, sprintf } from '~/locale';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
@@ -74,10 +75,21 @@ export default {
: '';
},
},
+ mounted() {
+ Mousetrap.bind('b', this.copyBranchName);
+ },
+ beforeDestroy() {
+ Mousetrap.unbind('b');
+ },
+ methods: {
+ copyBranchName() {
+ this.$refs.copyBranchNameButton.$el.click();
+ },
+ },
};
</script>
<template>
- <div class="d-flex mr-source-target append-bottom-default">
+ <div class="d-flex mr-source-target gl-mb-3">
<mr-widget-icon name="git-merge" />
<div class="git-merge-container d-flex">
<div class="normal">
@@ -89,6 +101,7 @@ export default {
class="label-branch label-truncate js-source-branch"
v-html="mr.sourceBranchLink"
/><clipboard-button
+ ref="copyBranchNameButton"
:text="branchNameClipboardData"
:title="__('Copy branch name')"
css-class="btn-default btn-transparent btn-clipboard"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
index 57d4d8b7ae6..e1659d9a167 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
@@ -13,7 +13,7 @@ export default {
</script>
<template>
- <div class="circle-icon-container append-right-default align-self-start align-self-lg-center">
+ <div class="circle-icon-container gl-mr-3 align-self-start align-self-lg-center">
<icon :name="name" :size="24" />
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 6df53311ef0..a096eb1a1fe 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -1,21 +1,22 @@
<script>
/* eslint-disable vue/require-default-prop */
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { GlIcon, GlLink, GlLoadingIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
-import { sprintf, s__ } from '~/locale';
-import PipelineStage from '~/pipelines/components/stage.vue';
+import { s__ } from '~/locale';
+import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default {
name: 'MRWidgetPipeline',
components: {
- PipelineStage,
CiIcon,
- Icon,
- TooltipOnTruncate,
GlLink,
+ GlLoadingIcon,
+ GlIcon,
+ GlSprintf,
+ PipelineStage,
+ TooltipOnTruncate,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
@@ -54,7 +55,11 @@ export default {
type: String,
required: false,
},
- troubleshootingDocsPath: {
+ mrTroubleshootingDocsPath: {
+ type: String,
+ required: true,
+ },
+ ciTroubleshootingDocsPath: {
type: String,
required: true,
},
@@ -64,10 +69,7 @@ export default {
return this.pipeline && Object.keys(this.pipeline).length > 0;
},
hasCIError() {
- return (this.hasCi && !this.ciStatus) || this.hasPipelineMustSucceedConflict;
- },
- hasPipelineMustSucceedConflict() {
- return !this.hasCi && this.pipelineMustSucceed;
+ return this.hasPipeline && !this.ciStatus;
},
status() {
return this.pipeline.details && this.pipeline.details.status
@@ -82,22 +84,6 @@ export default {
hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
},
- errorText() {
- if (this.hasPipelineMustSucceedConflict) {
- return s__('Pipeline|No pipeline has been run for this commit.');
- }
-
- return sprintf(
- s__(
- 'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}.',
- ),
- {
- linkStart: `<a href="${this.troubleshootingDocsPath}">`,
- linkEnd: '</a>',
- },
- false,
- );
- },
isTriggeredByMergeRequest() {
return Boolean(this.pipeline.merge_request);
},
@@ -118,31 +104,69 @@ export default {
return '';
},
},
+ errorText: s__(
+ 'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}.',
+ ),
+ monitoringPipelineText: s__('Pipeline|Checking pipeline status.'),
};
</script>
<template>
- <div class="ci-widget media js-ci-widget">
- <template v-if="!hasPipeline || hasCIError">
- <div class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error">
- <icon :size="24" name="status_failed_borderless" />
+ <div class="ci-widget media">
+ <template v-if="hasCIError">
+ <gl-icon name="status_failed" class="gl-text-red-500" :size="24" />
+ <div
+ class="gl-flex-fill-1 gl-ml-5"
+ tabindex="0"
+ role="text"
+ :aria-label="$options.errorText"
+ data-testid="ci-error-message"
+ >
+ <gl-sprintf :message="$options.errorText">
+ <template #link="{content}">
+ <gl-link :href="mrTroubleshootingDocsPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </template>
+ <template v-else-if="!hasPipeline">
+ <gl-loading-icon size="md" />
+ <div class="gl-flex-fill-1 gl-display-flex gl-ml-5" data-testid="monitoring-pipeline-message">
+ <span tabindex="0" role="text" :aria-label="$options.monitoringPipelineText">
+ <gl-sprintf :message="$options.monitoringPipelineText" />
+ </span>
+ <gl-link
+ :href="ciTroubleshootingDocsPath"
+ target="_blank"
+ class="gl-display-flex gl-align-items-center gl-ml-2"
+ tabindex="0"
+ >
+ <gl-icon
+ name="question"
+ :small="12"
+ tabindex="0"
+ role="text"
+ :aria-label="__('Link to go to GitLab pipeline documentation')"
+ />
+ </gl-link>
</div>
- <div class="media-body prepend-left-default" v-html="errorText"></div>
</template>
<template v-else-if="hasPipeline">
- <a :href="status.details_path" class="align-self-start append-right-default">
+ <a :href="status.details_path" class="align-self-start gl-mr-3">
<ci-icon :status="status" :size="24" :borderless="true" class="add-border" />
</a>
<div class="ci-widget-container d-flex">
<div class="ci-widget-content">
<div class="media-body">
<div
- class="font-weight-bold js-pipeline-info-container"
+ class="gl-font-weight-bold"
+ data-testid="pipeline-info-container"
data-qa-selector="merge_request_pipeline_info_content"
>
{{ pipeline.details.name }}
<gl-link
:href="pipeline.path"
- class="pipeline-id font-weight-normal pipeline-number"
+ class="pipeline-id gl-font-weight-normal pipeline-number"
+ data-testid="pipeline-id"
data-qa-selector="pipeline_link"
>#{{ pipeline.id }}</gl-link
>
@@ -151,7 +175,8 @@ export default {
{{ s__('Pipeline|for') }}
<gl-link
:href="pipeline.commit.commit_path"
- class="commit-sha js-commit-link font-weight-normal"
+ class="commit-sha gl-font-weight-normal"
+ data-testid="commit-link"
>{{ pipeline.commit.short_id }}</gl-link
>
</template>
@@ -160,18 +185,18 @@ export default {
<tooltip-on-truncate
:title="sourceBranch"
truncate-target="child"
- class="label-branch label-truncate font-weight-normal"
+ class="label-branch label-truncate gl-font-weight-normal"
v-html="sourceBranchLink"
/>
</template>
</div>
- <div v-if="pipeline.coverage" class="coverage">
+ <div v-if="pipeline.coverage" class="coverage" data-testid="pipeline-coverage">
{{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}%
<span
v-if="pipelineCoverageDelta"
- class="js-pipeline-coverage-delta"
:class="coverageDeltaClass"
+ data-testid="pipeline-coverage-delta"
>
({{ pipelineCoverageDelta }}%)
</span>
@@ -189,13 +214,13 @@ export default {
:class="{
'has-downstream': hasDownstream(i),
}"
- class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
+ class="stage-container dropdown mr-widget-pipeline-stages"
+ data-testid="widget-mini-pipeline-graph"
>
<pipeline-stage :stage="stage" />
</div>
</template>
</span>
-
<linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" />
</span>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 8fba0e2981f..5c307b5ff0c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -82,7 +82,8 @@ export default {
:pipeline-must-succeed="mr.onlyAllowMergeIfPipelineSucceeds"
:source-branch="branch"
:source-branch-link="branchLink"
- :troubleshooting-docs-path="mr.troubleshootingDocsPath"
+ :mr-troubleshooting-docs-path="mr.mrTroubleshootingDocsPath"
+ :ci-troubleshooting-docs-path="mr.ciTroubleshootingDocsPath"
/>
<template #footer>
<div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index d0df8309dc7..82566682bca 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -33,7 +33,7 @@ export default {
</script>
<template>
<div class="d-flex align-self-start">
- <div class="square s24 h-auto d-flex-center append-right-default">
+ <div class="square s24 h-auto d-flex-center gl-mr-3">
<div v-if="isLoading" class="mr-widget-icon d-inline-flex">
<gl-loading-icon size="md" class="mr-loading-icon d-inline-flex" />
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index 9942861d9e4..de01821a292 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -1,22 +1,31 @@
<script>
-import { GlLink, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlSprintf, GlButton } from '@gitlab/ui';
import MrWidgetIcon from './mr_widget_icon.vue';
-import PipelineTourState from './states/mr_widget_pipeline_tour.vue';
+import Tracking from '~/tracking';
+import { s__ } from '~/locale';
+
+const trackingMixin = Tracking.mixin();
+const TRACK_LABEL = 'no_pipeline_noticed';
export default {
name: 'MRWidgetSuggestPipeline',
iconName: 'status_notfound',
- popoverTarget: 'suggest-popover',
- popoverContainer: 'suggest-pipeline',
- trackLabel: 'no_pipeline_noticed',
+ trackLabel: TRACK_LABEL,
linkTrackValue: 30,
linkTrackEvent: 'click_link',
+ showTrackValue: 10,
+ showTrackEvent: 'click_button',
+ helpContent: s__(
+ `mrWidget|Use %{linkStart}CI pipelines to test your code%{linkEnd} by simply adding a GitLab CI configuration file to your project. It only takes a minute to make your code more secure and robust.`,
+ ),
+ helpURL: 'https://about.gitlab.com/blog/2019/07/12/guide-to-ci-cd-pipelines/',
components: {
GlLink,
GlSprintf,
+ GlButton,
MrWidgetIcon,
- PipelineTourState,
},
+ mixins: [trackingMixin],
props: {
pipelinePath: {
type: String,
@@ -31,45 +40,89 @@ export default {
required: true,
},
},
+ computed: {
+ tracking() {
+ return {
+ label: TRACK_LABEL,
+ property: this.humanAccess,
+ };
+ },
+ },
+ mounted() {
+ this.track();
+ },
};
</script>
<template>
- <div :id="$options.popoverContainer" class="d-flex mr-pipeline-suggest append-bottom-default">
- <mr-widget-icon :name="$options.iconName" />
- <div :id="$options.popoverTarget">
- <gl-sprintf
- :message="
- s__(`mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd}
+ <div class="mr-widget-body mr-pipeline-suggest gl-mb-3">
+ <div class="gl-display-flex gl-align-items-center">
+ <mr-widget-icon :name="$options.iconName" />
+ <div>
+ <gl-sprintf
+ :message="
+ s__(`mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd}
%{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd}
to create one.`)
- "
- >
- <template #prefixToLink="{content}">
+ "
+ >
+ <template #prefixToLink="{content}">
+ <strong>
+ {{ content }}
+ </strong>
+ </template>
+ <template #addPipelineLink="{content}">
+ <gl-link
+ :href="pipelinePath"
+ class="gl-ml-1"
+ data-testid="add-pipeline-link"
+ :data-track-property="humanAccess"
+ :data-track-value="$options.linkTrackValue"
+ :data-track-event="$options.linkTrackEvent"
+ :data-track-label="$options.trackLabel"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-5 order-md-last col-12 gl-mt-5 mt-md-n3 svg-content svg-225">
+ <img data-testid="pipeline-image" :src="pipelineSvgPath" />
+ </div>
+ <div class="col-md-7 order-md-first col-12">
+ <div class="ml-6 gl-pt-5">
<strong>
- {{ content }}
+ {{ s__('mrWidget|Are you adding technical debt or code vulnerabilities?') }}
</strong>
- </template>
- <template #addPipelineLink="{content}">
- <gl-link
+ <p class="gl-mt-2">
+ <gl-sprintf :message="$options.helpContent">
+ <template #link="{ content }">
+ <gl-link
+ data-testid="help"
+ :href="$options.helpURL"
+ target="_blank"
+ class="font-size-inherit"
+ >{{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <gl-button
+ data-testid="ok"
+ category="primary"
+ class="gl-mt-2"
+ variant="info"
:href="pipelinePath"
- class="ml-2 js-add-pipeline-path"
:data-track-property="humanAccess"
- :data-track-value="$options.linkTrackValue"
- :data-track-event="$options.linkTrackEvent"
+ :data-track-value="$options.showTrackValue"
+ :data-track-event="$options.showTrackEvent"
:data-track-label="$options.trackLabel"
>
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- <pipeline-tour-state
- :pipeline-path="pipelinePath"
- :pipeline-svg-path="pipelineSvgPath"
- :human-access="humanAccess"
- :popover-target="$options.popoverTarget"
- :popover-container="$options.popoverContainer"
- :track-label="$options.trackLabel"
- />
+ {{ __('Show me how to add a pipeline') }}
+ </gl-button>
+ </div>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_terraform_plan.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_terraform_plan.vue
deleted file mode 100644
index 2ef5e81b36b..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_terraform_plan.vue
+++ /dev/null
@@ -1,139 +0,0 @@
-<script>
-import { __ } from '~/locale';
-import { GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
-import Poll from '~/lib/utils/poll';
-
-export default {
- name: 'MRWidgetTerraformPlan',
- components: {
- GlIcon,
- GlLink,
- GlLoadingIcon,
- GlSprintf,
- },
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- loading: true,
- plans: {},
- };
- },
- computed: {
- addNum() {
- return Number(this.plan.create);
- },
- changeNum() {
- return Number(this.plan.update);
- },
- deleteNum() {
- return Number(this.plan.delete);
- },
- logUrl() {
- return this.plan.job_path;
- },
- plan() {
- const firstPlanKey = Object.keys(this.plans)[0];
- return this.plans[firstPlanKey] ?? {};
- },
- validPlanValues() {
- return this.addNum + this.changeNum + this.deleteNum >= 0;
- },
- },
- created() {
- this.fetchPlans();
- },
- methods: {
- fetchPlans() {
- this.loading = true;
-
- const poll = new Poll({
- resource: {
- fetchPlans: () => axios.get(this.endpoint),
- },
- data: this.endpoint,
- method: 'fetchPlans',
- successCallback: ({ data }) => {
- this.plans = data;
-
- if (Object.keys(this.plan).length) {
- this.loading = false;
- poll.stop();
- }
- },
- errorCallback: () => {
- this.plans = {};
- this.loading = false;
- flash(__('An error occurred while loading terraform report'));
- },
- });
-
- poll.makeRequest();
- },
- },
-};
-</script>
-
-<template>
- <section class="mr-widget-section">
- <div class="mr-widget-body media d-flex flex-row">
- <span class="append-right-default align-self-start align-self-lg-center">
- <gl-icon name="status_warning" :size="24" />
- </span>
-
- <div class="d-flex flex-fill flex-column flex-md-row">
- <div class="terraform-mr-plan-text normal d-flex flex-column flex-lg-row">
- <p class="m-0 pr-1">{{ __('A terraform report was generated in your pipelines.') }}</p>
-
- <gl-loading-icon v-if="loading" size="md" />
-
- <p v-else-if="validPlanValues" class="m-0">
- <gl-sprintf
- :message="
- __(
- 'Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
- )
- "
- >
- <template #addNum>
- <strong>{{ addNum }}</strong>
- </template>
-
- <template #changeNum>
- <strong>{{ changeNum }}</strong>
- </template>
-
- <template #deleteNum>
- <strong>{{ deleteNum }}</strong>
- </template>
- </gl-sprintf>
- </p>
-
- <p v-else class="m-0">{{ __('Changes are unknown') }}</p>
- </div>
-
- <div class="terraform-mr-plan-actions">
- <gl-link
- v-if="logUrl"
- :href="logUrl"
- target="_blank"
- data-track-event="click_terraform_mr_plan_button"
- data-track-label="mr_widget_terraform_mr_plan_button"
- data-track-property="terraform_mr_plan_button"
- class="btn btn-sm js-terraform-report-link"
- rel="noopener"
- >
- {{ __('View full log') }}
- <gl-icon name="external-link" />
- </gl-link>
- </div>
- </div>
- </div>
- </section>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
index acd8037cfb2..44bdc4a3be8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
@@ -29,7 +29,7 @@ export default {
<textarea
:id="inputId"
:value="value"
- class="form-control js-gfm-input append-bottom-default commit-message-edit"
+ class="form-control js-gfm-input gl-mb-3 commit-message-edit"
dir="auto"
required="required"
rows="7"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
index e4f4032776b..d52e6d38ac6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
@@ -83,7 +83,7 @@ export default {
<gl-deprecated-button
:aria-label="ariaLabel"
variant="blank"
- class="commit-edit-toggle square s24 append-right-default"
+ class="commit-edit-toggle square s24 gl-mr-3"
@click.stop="toggle()"
>
<icon :name="collapseIcon" :size="16" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 92848e86e76..f02e0ac84da 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -87,7 +87,7 @@ export default {
<status-icon status="success" />
<div class="media-body">
<h4 class="d-flex align-items-start">
- <span class="append-right-10">
+ <span class="gl-mr-3">
<span class="js-status-text-before-author">{{ statusTextBeforeAuthor }}</span>
<mr-widget-author :author="mr.setToAutoMergeBy" />
<span class="js-status-text-after-author">{{ statusTextAfterAuthor }}</span>
@@ -113,9 +113,7 @@ export default {
{{ s__('mrWidget|The source branch will be deleted') }}
</p>
<p v-else class="d-flex align-items-start">
- <span class="append-right-10">{{
- s__('mrWidget|The source branch will not be deleted')
- }}</span>
+ <span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
index a5e3115397a..e02be6dc2f7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
@@ -12,7 +12,7 @@ export default {
<div class="mr-widget-body media">
<status-icon :show-disabled-button="true" status="loading" />
<div class="media-body space-children">
- <span class="bold"> {{ s__('mrWidget|Checking ability to merge automatically…') }} </span>
+ <span class="bold"> {{ s__('mrWidget|Checking if merge request can be merged…') }} </span>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue
deleted file mode 100644
index f6bfb178437..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-<script>
-import { GlPopover, GlDeprecatedButton } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
-import Cookies from 'js-cookie';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import Tracking from '~/tracking';
-
-const trackingMixin = Tracking.mixin();
-
-const cookieKey = 'suggest_pipeline_dismissed';
-
-export default {
- name: 'MRWidgetPipelineTour',
- dismissTrackValue: 20,
- showTrackValue: 10,
- trackEvent: 'click_button',
- components: {
- GlPopover,
- GlDeprecatedButton,
- Icon,
- },
- mixins: [trackingMixin],
- props: {
- pipelinePath: {
- type: String,
- required: true,
- },
- pipelineSvgPath: {
- type: String,
- required: true,
- },
- humanAccess: {
- type: String,
- required: true,
- },
- popoverTarget: {
- type: String,
- required: true,
- },
- popoverContainer: {
- type: String,
- required: true,
- },
- trackLabel: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- popoverDismissed: parseBoolean(Cookies.get(cookieKey)),
- tracking: {
- label: this.trackLabel,
- property: this.humanAccess,
- },
- };
- },
- mounted() {
- this.trackOnShow();
- },
- methods: {
- trackOnShow() {
- if (!this.popoverDismissed) {
- this.track();
- }
- },
- dismissPopover() {
- this.popoverDismissed = true;
- Cookies.set(cookieKey, this.popoverDismissed, { expires: 365 });
- },
- },
-};
-</script>
-<template>
- <gl-popover
- v-if="!popoverDismissed"
- show
- :target="popoverTarget"
- :container="popoverContainer"
- placement="rightbottom"
- >
- <template #title>
- <button
- class="btn-blank float-right mt-1"
- type="button"
- :aria-label="__('Close')"
- :data-track-property="humanAccess"
- :data-track-value="$options.dismissTrackValue"
- :data-track-event="$options.trackEvent"
- :data-track-label="trackLabel"
- @click="dismissPopover"
- >
- <icon name="close" aria-hidden="true" />
- </button>
- {{ s__('mrWidget|Are you adding technical debt or code vulnerabilities?') }}
- </template>
- <div class="svg-content svg-150 pt-1">
- <img :src="pipelineSvgPath" />
- </div>
- <p>
- {{
- s__(
- 'mrWidget|Detect issues before deployment with a CI pipeline that continuously tests your code. We created a quick guide that will show you how to create one. Make your code more secure and more robust in just a minute.',
- )
- }}
- </p>
- <gl-deprecated-button
- ref="ok"
- category="primary"
- class="mt-2 mb-0"
- variant="info"
- block
- :href="pipelinePath"
- :data-track-property="humanAccess"
- :data-track-value="$options.showTrackValue"
- :data-track-event="$options.trackEvent"
- :data-track-label="trackLabel"
- >
- {{ __('Show me how') }}
- </gl-deprecated-button>
- <gl-deprecated-button
- ref="no-thanks"
- category="secondary"
- class="mt-2 mb-0"
- variant="info"
- block
- :data-track-property="humanAccess"
- :data-track-value="$options.dismissTrackValue"
- :data-track-event="$options.trackEvent"
- :data-track-label="trackLabel"
- @click="dismissPopover"
- >
- {{ __("No thanks, don't show this again") }}
- </gl-deprecated-button>
- </gl-popover>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 82be5eeb5ff..cc43135f50a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -45,7 +45,8 @@ export default {
isMakingRequest: false,
isMergingImmediately: false,
commitMessage: this.mr.commitMessage,
- squashBeforeMerge: this.mr.squash,
+ squashBeforeMerge: this.mr.squashIsSelected,
+ isSquashReadOnly: this.mr.squashIsReadonly,
successSvg,
warningSvg,
squashCommitMessage: this.mr.squashCommitMessage,
@@ -106,7 +107,12 @@ export default {
return this.isMergeButtonDisabled;
},
shouldShowSquashBeforeMerge() {
- const { commitsCount, enableSquashBeforeMerge } = this.mr;
+ const { commitsCount, enableSquashBeforeMerge, squashIsReadonly, squashIsSelected } = this.mr;
+
+ if (squashIsReadonly && !squashIsSelected) {
+ return false;
+ }
+
return enableSquashBeforeMerge && commitsCount > 1;
},
shouldShowMergeControls() {
@@ -344,21 +350,24 @@ export default {
v-if="shouldShowSquashBeforeMerge"
v-model="squashBeforeMerge"
:help-path="mr.squashBeforeMergeHelpPath"
- :is-disabled="isMergeButtonDisabled"
+ :is-disabled="isSquashReadOnly"
/>
</template>
<template v-else>
<div class="bold js-resolve-mr-widget-items-message">
- <gl-sprintf
+ <div
v-if="hasPipelineMustSucceedConflict"
- :message="pipelineMustSucceedConflictText"
+ class="gl-display-flex gl-align-items-center"
>
- <template #link="{ content }">
- <gl-link :href="mr.pipelineMustSucceedDocsPath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
+ <gl-sprintf :message="pipelineMustSucceedConflictText" />
+ <gl-link
+ :href="mr.pipelineMustSucceedDocsPath"
+ target="_blank"
+ class="gl-display-flex gl-ml-2"
+ >
+ <gl-icon name="question" />
+ </gl-link>
+ </div>
<gl-sprintf v-else :message="mergeDisabledText" />
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index 5305894873f..efd58341a2d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -1,6 +1,7 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
+import { __ } from '~/locale';
export default {
components: {
@@ -25,12 +26,22 @@ export default {
default: false,
},
},
+ computed: {
+ tooltipTitle() {
+ return this.isDisabled ? __('Required in this project.') : false;
+ },
+ },
};
</script>
<template>
<div class="inline">
- <label>
+ <label
+ v-tooltip
+ :class="{ 'gl-text-gray-600': isDisabled }"
+ data-testid="squashLabel"
+ :data-title="tooltipTitle"
+ >
<input
:checked="value"
:disabled="isDisabled"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
new file mode 100644
index 00000000000..f6e21dc1ec1
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
@@ -0,0 +1,140 @@
+<script>
+import { n__ } from '~/locale';
+import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue';
+import Poll from '~/lib/utils/poll';
+import TerraformPlan from './terraform_plan.vue';
+
+export default {
+ name: 'MRWidgetTerraformContainer',
+ components: {
+ GlSkeletonLoading,
+ GlSprintf,
+ MrWidgetExpanableSection,
+ TerraformPlan,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: true,
+ plansObject: {},
+ poll: null,
+ };
+ },
+ computed: {
+ inValidPlanCountText() {
+ if (this.numberOfInvalidPlans === 0) {
+ return null;
+ }
+
+ return n__(
+ 'Terraform|%{number} Terraform report failed to generate',
+ 'Terraform|%{number} Terraform reports failed to generate',
+ this.numberOfInvalidPlans,
+ );
+ },
+ numberOfInvalidPlans() {
+ return Object.values(this.plansObject).filter(plan => plan.tf_report_error).length;
+ },
+ numberOfPlans() {
+ return Object.keys(this.plansObject).length;
+ },
+ numberOfValidPlans() {
+ return this.numberOfPlans - this.numberOfInvalidPlans;
+ },
+ validPlanCountText() {
+ if (this.numberOfValidPlans === 0) {
+ return null;
+ }
+
+ return n__(
+ 'Terraform|%{number} Terraform report was generated in your pipelines',
+ 'Terraform|%{number} Terraform reports were generated in your pipelines',
+ this.numberOfValidPlans,
+ );
+ },
+ },
+ created() {
+ this.fetchPlans();
+ },
+ beforeDestroy() {
+ this.poll.stop();
+ },
+ methods: {
+ fetchPlans() {
+ this.loading = true;
+
+ this.poll = new Poll({
+ resource: {
+ fetchPlans: () => axios.get(this.endpoint),
+ },
+ data: this.endpoint,
+ method: 'fetchPlans',
+ successCallback: ({ data }) => {
+ this.plansObject = data;
+
+ if (this.numberOfPlans > 0) {
+ this.loading = false;
+ this.poll.stop();
+ }
+ },
+ errorCallback: () => {
+ this.plansObject = { bad_plan: { tf_report_error: 'api_error' } };
+ this.loading = false;
+ this.poll.stop();
+ },
+ });
+
+ this.poll.makeRequest();
+ },
+ },
+};
+</script>
+
+<template>
+ <section class="mr-widget-section">
+ <div v-if="loading" class="mr-widget-body">
+ <gl-skeleton-loading />
+ </div>
+
+ <mr-widget-expanable-section v-else>
+ <template #header>
+ <div
+ data-testid="terraform-header-text"
+ class="gl-flex-fill-1 gl-display-flex gl-flex-direction-column"
+ >
+ <p v-if="validPlanCountText" class="gl-m-0">
+ <gl-sprintf :message="validPlanCountText">
+ <template #number>
+ <strong>{{ numberOfValidPlans }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <p v-if="inValidPlanCountText" class="gl-m-0">
+ <gl-sprintf :message="inValidPlanCountText">
+ <template #number>
+ <strong>{{ numberOfInvalidPlans }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+ </template>
+
+ <template #content>
+ <terraform-plan
+ v-for="(plan, key) in plansObject"
+ :key="key"
+ :plan="plan"
+ class="mr-widget-body"
+ />
+ </template>
+ </mr-widget-expanable-section>
+ </section>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
new file mode 100644
index 00000000000..dc16d46dd8e
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
@@ -0,0 +1,111 @@
+<script>
+import { s__ } from '~/locale';
+import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+
+export default {
+ name: 'TerraformPlan',
+ components: {
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ },
+ props: {
+ plan: {
+ required: true,
+ type: Object,
+ },
+ },
+ computed: {
+ addNum() {
+ return Number(this.plan.create);
+ },
+ changeNum() {
+ return Number(this.plan.update);
+ },
+ deleteNum() {
+ return Number(this.plan.delete);
+ },
+ iconType() {
+ return this.validPlanValues ? 'doc-changes' : 'warning';
+ },
+ reportChangeText() {
+ if (this.validPlanValues) {
+ return s__(
+ 'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
+ );
+ }
+
+ return s__('Terraform|Generating the report caused an error.');
+ },
+ reportHeaderText() {
+ if (this.validPlanValues) {
+ return this.plan.job_name
+ ? s__('Terraform|The Terraform report %{name} was generated in your pipelines.')
+ : s__('Terraform|A Terraform report was generated in your pipelines.');
+ }
+
+ return this.plan.job_name
+ ? s__('Terraform|The Terraform report %{name} failed to generate.')
+ : s__('Terraform|A Terraform report failed to generate.');
+ },
+ validPlanValues() {
+ return this.addNum + this.changeNum + this.deleteNum >= 0;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex">
+ <span
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-mr-3 gl-align-self-start gl-mt-1"
+ >
+ <gl-icon :name="iconType" :size="18" data-testid="change-type-icon" />
+ </span>
+
+ <div class="gl-display-flex gl-flex-fill-1 gl-flex-direction-column flex-md-row">
+ <div class="gl-flex-fill-1 gl-display-flex gl-flex-direction-column">
+ <p class="gl-m-0 gl-pr-1">
+ <gl-sprintf :message="reportHeaderText">
+ <template #name>
+ <strong>{{ plan.job_name }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <p class="gl-m-0">
+ <gl-sprintf :message="reportChangeText">
+ <template #addNum>
+ <strong>{{ addNum }}</strong>
+ </template>
+
+ <template #changeNum>
+ <strong>{{ changeNum }}</strong>
+ </template>
+
+ <template #deleteNum>
+ <strong>{{ deleteNum }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+
+ <div>
+ <gl-link
+ v-if="plan.job_path"
+ :href="plan.job_path"
+ target="_blank"
+ data-testid="terraform-report-link"
+ data-track-event="click_terraform_mr_plan_button"
+ data-track-label="mr_widget_terraform_mr_plan_button"
+ data-track-property="terraform_mr_plan_button"
+ class="btn btn-sm"
+ rel="noopener"
+ >
+ {{ __('View full log') }}
+ <gl-icon name="external-link" />
+ </gl-link>
+ </div>
+ </div>
+ </div>
+</template>