diff options
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget')
24 files changed, 384 insertions, 87 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 index 386ba2e2d77..24cefd63ce3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue @@ -3,6 +3,7 @@ import { GlButton } from '@gitlab/ui'; import createFlash from '~/flash'; import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import { s__ } from '~/locale'; +import sidebarEventHub from '~/sidebar/event_hub'; import eventHub from '../../event_hub'; import approvalsMixin from '../../mixins/approvals'; import MrWidgetContainer from '../mr_widget_container.vue'; @@ -172,6 +173,7 @@ export default { this.mr.setApprovals(data); eventHub.$emit('MRWidgetUpdateRequested'); eventHub.$emit('ApprovalUpdated'); + sidebarEventHub.$emit('removeCurrentUserAttentionRequested'); this.$emit('updated'); }) .catch(errFn) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue index 33a83aef057..d878a1fa2e0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue @@ -70,6 +70,7 @@ export default { variant="confirm" size="small" class="gl-display-none gl-md-display-block gl-float-left" + data-testid="extension-actions-button" @click="onClickAction(btn)" > {{ btn.text }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index 549cf64fb08..7322958e6df 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -13,6 +13,7 @@ import * as Sentry from '@sentry/browser'; import api from '~/api'; import { sprintf, s__, __ } from '~/locale'; import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; +import Poll from '~/lib/utils/poll'; import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants'; import StatusIcon from './status_icon.vue'; import Actions from './actions.vue'; @@ -132,19 +133,50 @@ export default { this.triggerRedisTracking(); }, + initExtensionPolling() { + const poll = new Poll({ + resource: { + fetchData: () => this.fetchCollapsedData(this.$props), + }, + method: 'fetchData', + successCallback: (data) => { + if (Object.keys(data).length > 0) { + poll.stop(); + this.setCollapsedData(data); + } + }, + errorCallback: (e) => { + poll.stop(); + + this.setCollapsedError(e); + }, + }); + + poll.makeRequest(); + }, loadCollapsedData() { this.loadingState = LOADING_STATES.collapsedLoading; - this.fetchCollapsedData(this.$props) - .then((data) => { - this.collapsedData = data; - this.loadingState = null; - }) - .catch((e) => { - this.loadingState = LOADING_STATES.collapsedError; + if (this.$options.enablePolling) { + this.initExtensionPolling(); + } else { + this.fetchCollapsedData(this.$props) + .then((data) => { + this.setCollapsedData(data); + }) + .catch((e) => { + this.setCollapsedError(e); + }); + } + }, + setCollapsedData(data) { + this.collapsedData = data; + this.loadingState = null; + }, + setCollapsedError(e) { + this.loadingState = LOADING_STATES.collapsedError; - Sentry.captureException(e); - }); + Sentry.captureException(e); }, loadAllData() { if (this.hasFullData) return; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js index ec6e6ed2620..8438f3492b2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js @@ -13,6 +13,7 @@ export const registerExtension = (extension) => { props: extension.props, i18n: extension.i18n, expandEvent: extension.expandEvent, + enablePolling: extension.enablePolling, computed: { ...Object.keys(extension.computed).reduce( (acc, computedKey) => ({ 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 235a200b747..8cdaa3316ee 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 @@ -119,6 +119,8 @@ export default { :show-gitpod-button="mr.showGitpodButton" :gitpod-url="mr.gitpodUrl" :gitpod-enabled="mr.gitpodEnabled" + :user-preferences-gitpod-path="mr.userPreferencesGitpodPath" + :user-profile-enable-gitpod-path="mr.userProfileEnableGitpodPath" :gitpod-text="$options.i18n.gitpodText" class="gl-display-none gl-md-display-inline-block gl-mr-3" data-placement="bottom" 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 677c50ed930..2e3a02b1712 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 @@ -1,5 +1,6 @@ <script> import { GlButton, GlLoadingIcon } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import ciIcon from '../../vue_shared/components/ci_icon.vue'; export default { @@ -8,6 +9,7 @@ export default { GlButton, GlLoadingIcon, }, + mixins: [glFeatureFlagMixin()], props: { status: { type: String, @@ -42,7 +44,7 @@ export default { </div> <gl-button - v-if="showDisabledButton" + v-if="!glFeatures.restructuredMrWidget && showDisabledButton" category="primary" variant="success" data-testid="disabled-merge-button" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue index ce572f8b0bf..701ef89304c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue @@ -1,20 +1,16 @@ <script> -import { GlButton } from '@gitlab/ui'; import { s__ } from '~/locale'; -import notesEventHub from '~/notes/event_hub'; import StatusIcon from '../mr_widget_status_icon.vue'; export default { i18n: { - pipelineFailed: s__( - 'mrWidget|The pipeline for this merge request did not complete. Push a new commit to fix the failure.', - ), approvalNeeded: s__('mrWidget|Merge blocked: this merge request must be approved.'), - unresolvedDiscussions: s__('mrWidget|Merge blocked: all threads must be resolved.'), + blockingMergeRequests: s__( + 'mrWidget|Merge blocked: you can only merge after the above items are resolved.', + ), }, components: { StatusIcon, - GlButton, }, props: { mr: { @@ -24,22 +20,15 @@ export default { }, computed: { failedText() { - if (this.mr.isPipelineFailed) { - return this.$options.i18n.pipelineFailed; - } else if (this.mr.approvals && !this.mr.isApproved) { + if (this.mr.approvals && !this.mr.isApproved) { return this.$options.i18n.approvalNeeded; - } else if (this.mr.hasMergeableDiscussionsState) { - return this.$options.i18n.unresolvedDiscussions; + } else if (this.mr.blockingMergeRequests?.total_count > 0) { + return this.$options.i18n.blockingMergeRequests; } return null; }, }, - methods: { - jumpToFirstUnresolvedDiscussion() { - notesEventHub.$emit('jumpToFirstUnresolvedDiscussion'); - }, - }, }; </script> @@ -48,28 +37,6 @@ export default { <status-icon status="warning" /> <p class="media-body gl-m-0! gl-font-weight-bold gl-text-black-normal!"> {{ failedText }} - <template v-if="failedText == $options.i18n.unresolvedDiscussions"> - <gl-button - class="gl-ml-3" - size="small" - variant="confirm" - data-testid="jumpToUnresolved" - @click="jumpToFirstUnresolvedDiscussion" - > - {{ s__('mrWidget|Jump to first unresolved thread') }} - </gl-button> - <gl-button - v-if="mr.createIssueToResolveDiscussionsPath" - :href="mr.createIssueToResolveDiscussionsPath" - class="gl-ml-3" - size="small" - variant="confirm" - category="secondary" - data-testid="resolveIssue" - > - {{ s__('mrWidget|Create issue to resolve all threads') }} - </gl-button> - </template> </p> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue index 13b1e49f44e..071920856a8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue @@ -1,25 +1,22 @@ <script> -import { GlButton } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetArchived', components: { - GlButton, statusIcon, }, + mixins: [glFeatureFlagMixin()], }; </script> <template> <div class="mr-widget-body media"> <div class="space-children"> - <status-icon status="warning" /> - <gl-button category="secondary" variant="success" :disabled="true"> - {{ s__('mrWidget|Merge') }} - </gl-button> + <status-icon status="warning" show-disabled-button /> </div> <div class="media-body"> - <span class="bold"> + <span :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" class="bold"> {{ s__('mrWidget|Merge unavailable: merge requests are read-only on archived projects.') }} </span> </div> 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 10b93d7849f..fd42fa0421f 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,9 +12,11 @@ export default { </script> <template> <div class="mr-widget-body media"> - <status-icon :show-disabled-button="!glFeatures.restructuredMrWidget" status="loading" /> + <status-icon :show-disabled-button="true" status="loading" /> <div class="media-body space-children"> - <span class="bold"> {{ s__('mrWidget|Checking if merge request can be merged…') }} </span> + <span :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" 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_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index 7a002d41ac0..a2c9cfe53cc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -109,14 +109,18 @@ export default { </gl-skeleton-loader> </div> <div v-else class="media-body space-children gl-display-flex gl-align-items-center"> - <span v-if="shouldBeRebased" class="bold"> + <span + v-if="shouldBeRebased" + :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" + class="bold" + > {{ s__(`mrWidget|Merge blocked: fast-forward merge is not possible. To merge this request, first rebase locally.`) }} </span> <template v-else> - <span class="bold"> + <span :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" class="bold"> {{ s__('mrWidget|Merge blocked: merge conflicts must be resolved.') }} <span v-if="!canMerge"> {{ @@ -129,6 +133,7 @@ export default { <gl-button v-if="showResolveButton" :href="mr.conflictResolutionPath" + :size="glFeatures.restructuredMrWidget ? 'small' : 'medium'" data-testid="resolve-conflicts-button" > {{ s__('mrWidget|Resolve conflicts') }} @@ -136,6 +141,7 @@ export default { <gl-button v-if="canMerge" v-gl-modal-directive="'modal-merge-info'" + :size="glFeatures.restructuredMrWidget ? 'small' : 'medium'" data-testid="merge-locally-button" > {{ s__('mrWidget|Merge locally') }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue index f91350d4a82..5b03eda2eac 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue @@ -74,10 +74,21 @@ export default { <status-icon :show-disabled-button="true" status="warning" /> <div class="media-body space-children"> - <span class="bold js-branch-text"> + <span + :class="{ + 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget, + }" + class="bold js-branch-text" + > <span class="capitalize" data-testid="missingBranchName"> {{ missingBranchName }} </span> {{ s__('mrWidget|branch does not exist.') }} {{ missingBranchNameMessage }} - <gl-icon v-gl-tooltip :title="message" :aria-label="message" name="question-o" /> + <gl-icon + v-gl-tooltip + :title="message" + :aria-label="message" + name="question-o" + class="gl-text-blue-600 gl-cursor-pointer" + /> </span> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue index 68ffca9cd68..34c5a2ff2c8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue @@ -1,4 +1,5 @@ <script> +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import StatusIcon from '../mr_widget_status_icon.vue'; export default { @@ -6,13 +7,14 @@ export default { components: { StatusIcon, }, + mixins: [glFeatureFlagMixin()], }; </script> <template> <div class="mr-widget-body media"> <status-icon :show-disabled-button="true" status="warning" /> <div class="media-body space-children"> - <span class="bold"> + <span :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" class="bold"> {{ s__( `mrWidget|Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.`, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 01e8303f513..bb0fb410d3e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -3,11 +3,13 @@ import { GlButton, GlSkeletonLoader } from '@gitlab/ui'; import createFlash from '~/flash'; import { __ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import ActionsButton from '~/vue_shared/components/actions_button.vue'; import simplePoll from '../../../lib/utils/simple_poll'; import eventHub from '../../event_hub'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import rebaseQuery from '../../queries/states/rebase.query.graphql'; import statusIcon from '../mr_widget_status_icon.vue'; +import { REBASE_BUTTON_KEY, REBASE_WITHOUT_CI_BUTTON_KEY } from '../../constants'; export default { name: 'MRWidgetRebase', @@ -25,8 +27,9 @@ export default { }, components: { statusIcon, - GlButton, GlSkeletonLoader, + ActionsButton, + GlButton, }, mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], props: { @@ -44,12 +47,16 @@ export default { state: {}, isMakingRequest: false, rebasingError: null, + selectedRebaseAction: REBASE_BUTTON_KEY, }; }, computed: { isLoading() { return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading; }, + showRebaseWithoutCi() { + return this.glFeatures?.rebaseWithoutCiUi; + }, rebaseInProgress() { if (this.glFeatures.mergeRequestWidgetGraphql) { return this.state.rebaseInProgress; @@ -86,14 +93,36 @@ export default { fastForwardMergeText() { return __('Merge blocked: the source branch must be rebased onto the target branch.'); }, + actions() { + return [this.rebaseAction, this.rebaseWithoutCiAction].filter((action) => action); + }, + rebaseAction() { + return { + key: REBASE_BUTTON_KEY, + text: __('Rebase'), + secondaryText: __('Rebases and triggers a pipeline'), + attrs: { + 'data-qa-selector': 'mr_rebase_button', + }, + handle: () => this.rebase(), + }; + }, + rebaseWithoutCiAction() { + return { + key: REBASE_WITHOUT_CI_BUTTON_KEY, + text: __('Rebase without CI'), + secondaryText: __('Performs a rebase but skips triggering a new pipeline'), + handle: () => this.rebase({ skipCi: true }), + }; + }, }, methods: { - rebase() { + rebase({ skipCi = false } = {}) { this.isMakingRequest = true; this.rebasingError = null; this.service - .rebase() + .rebase({ skipCi }) .then(() => { simplePoll(this.checkRebaseStatus); }) @@ -109,6 +138,9 @@ export default { } }); }, + selectRebaseAction(key) { + this.selectedRebaseAction = key; + }, checkRebaseStatus(continuePolling, stopPolling) { this.service .poll() @@ -152,12 +184,14 @@ export default { <div class="rebase-state-find-class-convention media media-body space-children"> <span v-if="rebaseInProgress || isMakingRequest" + :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" class="gl-font-weight-bold" data-testid="rebase-message" >{{ __('Rebase in progress') }}</span > <span v-if="!rebaseInProgress && !canPushToSourceBranch" + :class="{ 'gl-text-body!': glFeatures.restructuredMrWidget }" class="gl-font-weight-bold gl-ml-0!" data-testid="rebase-message" >{{ fastForwardMergeText }}</span @@ -167,15 +201,26 @@ export default { class="accept-merge-holder clearfix js-toggle-container accept-action media space-children" > <gl-button + v-if="!glFeatures.restructuredMrWidget && !showRebaseWithoutCi" :loading="isMakingRequest" variant="confirm" data-qa-selector="mr_rebase_button" + data-testid="standard-rebase-button" @click="rebase" > {{ __('Rebase') }} </gl-button> + <actions-button + v-if="!glFeatures.restructuredMrWidget && showRebaseWithoutCi" + :actions="actions" + :selected-key="selectedRebaseAction" + variant="confirm" + category="primary" + @select="selectRebaseAction" + /> <span v-if="!rebasingError" + :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" class="gl-font-weight-bold" data-testid="rebase-message" data-qa-selector="no_fast_forward_message_content" @@ -186,6 +231,17 @@ export default { <span v-else class="gl-font-weight-bold danger" data-testid="rebase-message">{{ rebasingError }}</span> + <gl-button + v-if="glFeatures.restructuredMrWidget" + :loading="isMakingRequest" + variant="confirm" + size="small" + data-qa-selector="mr_rebase_button" + class="gl-ml-3!" + @click="rebase" + > + {{ __('Rebase') }} + </gl-button> </div> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue index 2d704d3b07a..e43319d42ca 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue @@ -62,8 +62,8 @@ export default { <gl-button v-if="mr.newBlobPath" :href="mr.newBlobPath" - category="secondary" - variant="success" + category="primary" + variant="confirm" data-testid="createFileButton" @click="onClickNewFile" > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue index b5d2f91c637..d88dad2e086 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue @@ -1,6 +1,7 @@ <script> import { GlLink, GlSprintf } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { s__ } from '~/locale'; import statusIcon from '../mr_widget_status_icon.vue'; @@ -11,6 +12,7 @@ export default { GlSprintf, statusIcon, }, + mixins: [glFeatureFlagMixin()], computed: { troubleshootingDocsPath() { return helpPagePath('ci/troubleshooting', { anchor: 'merge-request-status-messages' }); @@ -28,7 +30,7 @@ export default { <div class="mr-widget-body media"> <status-icon :show-disabled-button="true" status="warning" /> <div class="media-body space-children"> - <span class="bold"> + <span :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" class="bold"> <gl-sprintf :message="$options.i18n.failedMessage"> <template #link="{ content }"> <gl-link :href="troubleshootingDocsPath" target="_blank"> 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 8830128b7d6..06ce312bd4c 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 @@ -529,7 +529,7 @@ export default { <template> <div :class="{ - 'gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7': + 'gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7 gl-rounded-bottom-left-base gl-rounded-bottom-right-base': glFeatures.restructuredMrWidget, }" > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue index 7eeba8d8f89..b1fbe150fcf 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue @@ -1,5 +1,6 @@ <script> import { GlButton } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { I18N_SHA_MISMATCH } from '../../i18n'; import statusIcon from '../mr_widget_status_icon.vue'; @@ -12,6 +13,7 @@ export default { i18n: { I18N_SHA_MISMATCH, }, + mixins: [glFeatureFlagMixin()], props: { mr: { type: Object, @@ -25,7 +27,11 @@ export default { <div class="mr-widget-body media"> <status-icon :show-disabled-button="false" status="warning" /> <div class="media-body"> - <span class="gl-font-weight-bold" data-qa-selector="head_mismatch_content"> + <span + :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" + class="gl-font-weight-bold" + data-qa-selector="head_mismatch_content" + > {{ $options.i18n.I18N_SHA_MISMATCH.warningMessage }} </span> <gl-button diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue index 69e4df0ca11..8cf6383c26a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue @@ -1,5 +1,6 @@ <script> import { GlButton } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import notesEventHub from '~/notes/event_hub'; import statusIcon from '../mr_widget_status_icon.vue'; @@ -9,6 +10,7 @@ export default { statusIcon, GlButton, }, + mixins: [glFeatureFlagMixin()], props: { mr: { type: Object, @@ -25,16 +27,24 @@ export default { <template> <div class="mr-widget-body media gl-flex-wrap"> - <status-icon :show-disabled-button="true" status="warning" /> + <status-icon show-disabled-button status="warning" /> <div class="media-body"> - <span class="gl-ml-3 gl-font-weight-bold gl-display-block gl-w-100">{{ - s__('mrWidget|Merge blocked: all threads must be resolved.') - }}</span> + <span + :class="{ + 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget, + 'gl-display-block': !glFeatures.restructuredMrWidget, + }" + class="gl-ml-3 gl-font-weight-bold gl-w-100" + > + {{ s__('mrWidget|Merge blocked: all threads must be resolved.') }} + </span> <gl-button data-testid="jump-to-first" class="gl-ml-3" size="small" - icon="comment-next" + :icon="glFeatures.restructuredMrWidget ? undefined : 'comment-next'" + :variant="glFeatures.restructuredMrWidget && 'confirm'" + :category="glFeatures.restructuredMrWidget && 'secondary'" @click="jumpToFirstUnresolvedDiscussion" > {{ s__('mrWidget|Jump to first unresolved thread') }} @@ -44,7 +54,7 @@ export default { :href="mr.createIssueToResolveDiscussionsPath" class="js-create-issue gl-ml-3" size="small" - icon="issue-new" + :icon="glFeatures.restructuredMrWidget ? undefined : 'issue-new'" > {{ s__('mrWidget|Create issue to resolve all threads') }} </gl-button> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index ba831a33b73..e0e19094c40 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -166,7 +166,10 @@ export default { <status-icon :show-disabled-button="canUpdate" status="warning" /> <div class="media-body"> <div class="float-left"> - <span class="gl-font-weight-bold"> + <span + :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" + class="gl-font-weight-bold" + > {{ __("Merge blocked: merge request must be marked as ready. It's still marked as draft.") }} diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js index 2edccce7f4e..32effb91043 100644 --- a/app/assets/javascripts/vue_merge_request_widget/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/constants.js @@ -162,3 +162,6 @@ export const EXTENSION_SUMMARY_FAILED_CLASS = 'gl-text-red-500'; export const EXTENSION_SUMMARY_NEUTRAL_CLASS = 'gl-text-gray-700'; export { STATE_MACHINE }; + +export const REBASE_BUTTON_KEY = 'rebase'; +export const REBASE_WITHOUT_CI_BUTTON_KEY = 'rebaseWithoutCi'; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js new file mode 100644 index 00000000000..a564acada02 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js @@ -0,0 +1,173 @@ +import { __, n__, s__, sprintf } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import { EXTENSION_ICONS } from '../../constants'; + +export default { + name: 'WidgetTerraform', + enablePolling: true, + i18n: { + label: s__('Terraform|Terraform reports'), + loading: s__('Terraform|Loading Terraform reports...'), + error: s__('Terraform|Failed to load Terraform reports'), + reportGenerated: s__('Terraform|A Terraform report was generated in your pipelines.'), + namedReportGenerated: s__( + 'Terraform|The job %{strong_start}%{name}%{strong_end} generated a report.', + ), + reportChanges: s__( + 'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete', + ), + reportFailed: s__('Terraform|A Terraform report failed to generate.'), + namedReportFailed: s__( + 'Terraform|The job %{strong_start}%{name}%{strong_end} failed to generate a report.', + ), + reportErrored: s__('Terraform|Generating the report caused an error.'), + fullLog: __('Full log'), + }, + expandEvent: 'i_testing_terraform_widget_total', + props: ['terraformReportsPath'], + computed: { + // Extension computed props + statusIcon() { + return EXTENSION_ICONS.warning; + }, + }, + methods: { + // Extension methods + summary({ valid = [], invalid = [] }) { + let title; + let subtitle = ''; + + const validText = sprintf( + n__( + 'Terraform|%{strong_start}%{number}%{strong_end} Terraform report was generated in your pipelines', + 'Terraform|%{strong_start}%{number}%{strong_end} Terraform reports were generated in your pipelines', + valid.length, + ), + { + number: valid.length, + }, + false, + ); + + const invalidText = sprintf( + n__( + 'Terraform|%{strong_start}%{number}%{strong_end} Terraform report failed to generate', + 'Terraform|%{strong_start}%{number}%{strong_end} Terraform reports failed to generate', + invalid.length, + ), + { + number: invalid.length, + }, + false, + ); + + if (valid.length) { + title = validText; + if (invalid.length) { + subtitle = sprintf(`<br>%{small_start}${invalidText}%{small_end}`); + } + } else { + title = invalidText; + } + + return `${title}${subtitle}`; + }, + fetchCollapsedData() { + return Promise.resolve(this.fetchPlans().then(this.prepareReports)); + }, + fetchFullData() { + const { valid, invalid } = this.collapsedData; + return Promise.resolve([...valid, ...invalid]); + }, + // Custom methods + fetchPlans() { + return axios + .get(this.terraformReportsPath) + .then(({ data }) => { + return Object.keys(data).map((key) => { + return data[key]; + }); + }) + .catch(() => { + const invalidData = { tf_report_error: 'api_error' }; + return [invalidData]; + }); + }, + createReportRow(report, iconName) { + const addNum = Number(report.create); + const changeNum = Number(report.update); + const deleteNum = Number(report.delete); + const validPlanValues = addNum + changeNum + deleteNum >= 0; + + const actions = []; + + let title; + let subtitle; + + if (report.job_path) { + const action = { + href: report.job_path, + text: this.$options.i18n.fullLog, + target: '_blank', + }; + actions.push(action); + } + + if (validPlanValues) { + if (report.job_name) { + title = sprintf( + this.$options.i18n.namedReportGenerated, + { + name: report.job_name, + }, + false, + ); + } else { + title = this.$options.i18n.reportGenerated; + } + + subtitle = sprintf(`%{small_start}${this.$options.i18n.reportChanges}%{small_end}`, { + addNum, + changeNum, + deleteNum, + }); + } else { + if (report.job_name) { + title = sprintf( + this.$options.i18n.namedReportFailed, + { + name: report.job_name, + }, + false, + ); + } else { + title = this.$options.i18n.reportFailed; + } + + subtitle = sprintf(`%{small_start}${this.$options.i18n.reportErrored}%{small_end}`); + } + + return { + text: `${title} + <br> + ${subtitle}`, + icon: { name: iconName }, + actions, + }; + }, + prepareReports(reports) { + const valid = []; + const invalid = []; + + reports.forEach((report) => { + if (report.tf_report_error) { + invalid.push(this.createReportRow(report, EXTENSION_ICONS.error)); + } else { + valid.push(this.createReportRow(report, EXTENSION_ICONS.success)); + } + }); + + return { valid, invalid }; + }, + }, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index c98dc426224..83a07240403 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -1,6 +1,7 @@ <script> import { GlSafeHtmlDirective } from '@gitlab/ui'; import { isEmpty } from 'lodash'; +import { registerExtension } from '~/vue_merge_request_widget/components/extensions'; import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue'; import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service'; import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store'; @@ -43,6 +44,7 @@ import { STATE_MACHINE, stateToComponentMap } from './constants'; import eventHub from './event_hub'; import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables'; import getStateQuery from './queries/get_state.query.graphql'; +import terraformExtension from './extensions/terraform'; export default { // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25 @@ -184,6 +186,9 @@ export default { shouldRenderSecurityReport() { return Boolean(this.mr.pipeline.id); }, + shouldRenderTerraformPlans() { + return Boolean(this.mr?.terraformReportsPath); + }, mergeError() { let { mergeError } = this.mr; @@ -230,6 +235,11 @@ export default { this.initPostMergeDeploymentsPolling(); } }, + shouldRenderTerraformPlans(newVal) { + if (newVal) { + this.registerTerraformPlans(); + } + }, }, mounted() { MRWidgetService.fetchInitialData() @@ -463,6 +473,11 @@ export default { dismissSuggestPipelines() { this.mr.isDismissedSuggestPipeline = true; }, + registerTerraformPlans() { + if (this.shouldRenderTerraformPlans && this.shouldShowExtension) { + registerExtension(terraformExtension); + } + }, }, }; </script> @@ -542,7 +557,10 @@ export default { :pipeline-path="mr.pipeline.path" /> - <terraform-plan v-if="mr.terraformReportsPath" :endpoint="mr.terraformReportsPath" /> + <terraform-plan + v-if="mr.terraformReportsPath && !shouldShowExtension" + :endpoint="mr.terraformReportsPath" + /> <grouped-accessibility-reports-app v-if="shouldShowAccessibilityReport" diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js index 7dcb4881e7f..7b803b0fcbb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js +++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js @@ -55,8 +55,9 @@ export default class MRWidgetService { return axios.get(this.endpoints.mergeActionsContentPath); } - rebase() { - return axios.post(this.endpoints.rebasePath); + rebase({ skipCi = false } = {}) { + const path = `${this.endpoints.rebasePath}?skip_ci=${Boolean(skipCi)}`; + return axios.post(path); } fetchApprovals() { diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 57af869a0ba..5378dabf638 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -222,6 +222,8 @@ export default class MergeRequestStore { this.showGitpodButton = data.show_gitpod_button; this.gitpodUrl = data.gitpod_url; this.gitpodEnabled = data.gitpod_enabled; + this.userPreferencesGitpodPath = data.user_preferences_gitpod_path; + this.userProfileEnableGitpodPath = data.user_profile_enable_gitpod_path; } setState() { @@ -357,15 +359,13 @@ export default class MergeRequestStore { setApprovals(data) { this.approvals = data; this.isApproved = data.approved || false; + + this.setState(); } + // eslint-disable-next-line class-methods-use-this get hasMergeChecksFailed() { - if (!window.gon?.features?.restructuredMrWidget) return false; - - return ( - this.hasMergeableDiscussionsState || - (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) - ); + return false; } // Because the state machine doesn't yet handle every state and transition, |