summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
authorFatih Acet <acetfatih@gmail.com>2017-05-09 04:15:34 +0000
committerJacob Schatz <jschatz@gitlab.com>2017-05-09 04:15:34 +0000
commit0151325dacebb99d54b6effb1d5842c0c712168c (patch)
tree767ea3c8cfeedab5a0ce1921d5842b149ea7c0be /app/assets/javascripts
parent9ada13d343a4736275d2c026ee980abe5c5e5763 (diff)
downloadgitlab-ce-0151325dacebb99d54b6effb1d5842c0c712168c.tar.gz
Merge request widget redesign
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js1
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js1
-rw-r--r--app/assets/javascripts/dispatcher.js6
-rw-r--r--app/assets/javascripts/lib/utils/simple_poll.js15
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/merge_request.js15
-rw-r--r--app/assets/javascripts/merge_request_widget/ci_bundle.js53
-rw-r--r--app/assets/javascripts/merged_buttons.js47
-rw-r--r--app/assets/javascripts/notes.js12
-rw-r--r--app/assets/javascripts/pipelines/components/stage.js104
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js118
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js98
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js109
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js76
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js42
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js76
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js116
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js130
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js34
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js309
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js59
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js42
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/event_hub.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js234
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js57
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js134
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js36
-rw-r--r--app/assets/javascripts/vue_shared/components/memory_graph.js36
-rw-r--r--app/assets/javascripts/vue_shared/components/pipeline_status_icon.js23
-rw-r--r--app/assets/javascripts/vue_shared/pipeline_svg_icons.js43
47 files changed, 2292 insertions, 110 deletions
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index 92f6fd654b3..9d51fb53eb2 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -88,6 +88,7 @@ const ResolveBtn = Vue.extend({
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
this.discussion.updateHeadline(data);
+ gl.mrWidget.checkStatus();
} else {
new Flash(errorFlashMsg);
}
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index 4ea6ba8a73d..ba4f6d36fcb 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -49,6 +49,7 @@ class ResolveServiceClass {
discussion.resolveAllNotes(resolved_by);
}
+ gl.mrWidget.checkStatus();
discussion.updateHeadline(data);
} else {
throw new Error('An error occurred when trying to resolve discussion.');
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index bf802056d36..abb871c3af0 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -10,7 +10,6 @@
/* global IssuableForm */
/* global LabelsSelect */
/* global MilestoneSelect */
-/* global MergedButtons */
/* global Commit */
/* global NotificationsForm */
/* global TreeView */
@@ -216,15 +215,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
new gl.Diff();
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
- new MergedButtons();
- break;
- case 'projects:merge_requests:commits':
- new MergedButtons();
break;
case "projects:merge_requests:diffs":
new gl.Diff();
new ZenMode();
- new MergedButtons();
break;
case 'dashboard:activity':
new gl.Activities();
diff --git a/app/assets/javascripts/lib/utils/simple_poll.js b/app/assets/javascripts/lib/utils/simple_poll.js
new file mode 100644
index 00000000000..25ca98afbe7
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/simple_poll.js
@@ -0,0 +1,15 @@
+export default (fn, interval = 2000, timeout = 60000) => {
+ const startTime = Date.now();
+
+ return new Promise((resolve, reject) => {
+ const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
+ const next = () => {
+ if (Date.now() - startTime < timeout) {
+ setTimeout(fn.bind(null, next, stop), interval);
+ } else {
+ reject(new Error('SIMPLE_POLL_TIMEOUT'));
+ }
+ };
+ fn(next, stop);
+ });
+};
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 1b0d5fc92e3..a07aa047293 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -123,8 +123,6 @@ import './member_expiration_date';
import './members';
import './merge_request';
import './merge_request_tabs';
-import './merge_request_widget';
-import './merged_buttons';
import './milestone';
import './milestone_select';
import './mini_pipeline_graph_dropdown';
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 5e01aacf2ba..ed342b9990f 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -106,6 +106,21 @@ require('./merge_request_tabs');
});
};
+ MergeRequest.prototype.updateStatusText = function(classToRemove, classToAdd, newStatusText) {
+ $('.detail-page-header .status-box')
+ .removeClass(classToRemove)
+ .addClass(classToAdd)
+ .find('span')
+ .text(newStatusText);
+ };
+
+ MergeRequest.prototype.decreaseCounter = function(by = 1) {
+ const $el = $('.nav-links .js-merge-counter');
+ const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
+
+ $el.text(gl.text.addDelimiter(count));
+ };
+
return MergeRequest;
})();
}).call(window);
diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js b/app/assets/javascripts/merge_request_widget/ci_bundle.js
deleted file mode 100644
index 21d7c3e168e..00000000000
--- a/app/assets/javascripts/merge_request_widget/ci_bundle.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/* global merge_request_widget */
-
-(() => {
- $(() => {
- /* TODO: This needs a better home, or should be refactored. It was previously contained
- * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml,
- * but Vue chokes on script tags and prevents their execution. So it was moved here
- * temporarily.
- * */
-
- $(document)
- .off('ajax:send', '.accept-mr-form')
- .on('ajax:send', '.accept-mr-form', () => {
- $('.accept-mr-form :input').disable();
- });
-
- $(document)
- .off('click', '.accept-merge-request')
- .on('click', '.accept-merge-request', () => {
- $('.js-merge-button, .js-merge-when-pipeline-succeeds-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
- });
-
- $(document)
- .off('click', '.merge-when-pipeline-succeeds')
- .on('click', '.merge-when-pipeline-succeeds', () => {
- $('#merge_when_pipeline_succeeds').val('1');
- });
-
- $(document)
- .off('click', '.js-merge-dropdown a')
- .on('click', '.js-merge-dropdown a', (e) => {
- e.preventDefault();
- $(e.target).closest('form').submit();
- });
- if ($('.rebase-in-progress').length) {
- merge_request_widget.rebaseInProgress();
- } else if ($('.rebase-mr-form').length) {
- $(document)
- .off('ajax:send', '.rebase-mr-form')
- .on('ajax:send', '.rebase-mr-form', () => {
- $('.rebase-mr-form :input').disable();
- });
-
- $(document)
- .off('click', '.js-rebase-button')
- .on('click', '.js-rebase-button', () => {
- $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress");
- });
- } else {
- setTimeout(() => merge_request_widget.getMergeStatus(), 200);
- }
- });
-})();
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
deleted file mode 100644
index 7b0997c6520..00000000000
--- a/app/assets/javascripts/merged_buttons.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len */
-
-import '~/lib/utils/url_utility';
-
-(function() {
- this.MergedButtons = (function() {
- function MergedButtons() {
- this.removeSourceBranch = this.removeSourceBranch.bind(this);
- this.removeBranchSuccess = this.removeBranchSuccess.bind(this);
- this.removeBranchError = this.removeBranchError.bind(this);
- this.$removeBranchWidget = $('.remove_source_branch_widget');
- this.$removeBranchProgress = $('.remove_source_branch_in_progress');
- this.$removeBranchFailed = $('.remove_source_branch_widget.failed');
- this.cleanEventListeners();
- this.initEventListeners();
- }
-
- MergedButtons.prototype.cleanEventListeners = function() {
- $(document).off('click', '.remove_source_branch');
- $(document).off('ajax:success', '.remove_source_branch');
- return $(document).off('ajax:error', '.remove_source_branch');
- };
-
- MergedButtons.prototype.initEventListeners = function() {
- $(document).on('click', '.remove_source_branch', this.removeSourceBranch);
- $(document).on('ajax:success', '.remove_source_branch', this.removeBranchSuccess);
- $(document).on('ajax:error', '.remove_source_branch', this.removeBranchError);
- };
-
- MergedButtons.prototype.removeSourceBranch = function() {
- this.$removeBranchWidget.hide();
- return this.$removeBranchProgress.show();
- };
-
- MergedButtons.prototype.removeBranchSuccess = function() {
- gl.utils.refreshCurrentPage();
- };
-
- MergedButtons.prototype.removeBranchError = function() {
- this.$removeBranchWidget.hide();
- this.$removeBranchProgress.hide();
- return this.$removeBranchFailed.show();
- };
-
- return MergedButtons;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 55391ebc089..d2e602a0763 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -276,7 +276,7 @@ const normalizeNewlines = function(str) {
var votesBlock;
if (noteEntity.commands_changes) {
if ('merge' in noteEntity.commands_changes) {
- $.get(mrRefreshWidgetUrl);
+ Notes.checkMergeRequestStatus();
}
if ('emoji_award' in noteEntity.commands_changes) {
@@ -424,6 +424,7 @@ const normalizeNewlines = function(str) {
}
gl.utils.localTimeAgo($('.js-timeago'), false);
+ Notes.checkMergeRequestStatus();
return this.updateNotesCount(1);
};
@@ -769,7 +770,8 @@ const normalizeNewlines = function(str) {
}
};
})(this));
- // Decrement the "Discussions" counter only once
+
+ Notes.checkMergeRequestStatus();
return this.updateNotesCount(-1);
};
@@ -1115,6 +1117,12 @@ const normalizeNewlines = function(str) {
return $form;
};
+ Notes.checkMergeRequestStatus = function() {
+ if (gl.utils.getPagePath(1) === 'merge_requests') {
+ gl.mrWidget.checkStatus();
+ }
+ };
+
Notes.animateAppendNote = function(noteHtml, $notesList) {
const $note = $(noteHtml);
diff --git a/app/assets/javascripts/pipelines/components/stage.js b/app/assets/javascripts/pipelines/components/stage.js
new file mode 100644
index 00000000000..034e8d3280e
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/stage.js
@@ -0,0 +1,104 @@
+/* global Flash */
+import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
+
+export default {
+ data() {
+ return {
+ builds: '',
+ spinner: '<span class="fa fa-spinner fa-spin"></span>',
+ };
+ },
+
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ updated() {
+ if (this.builds) {
+ this.stopDropdownClickPropagation();
+ }
+ },
+
+ methods: {
+ fetchBuilds(e) {
+ const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
+
+ if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
+
+ return this.$http.get(this.stage.dropdown_path)
+ .then((response) => {
+ this.builds = JSON.parse(response.body).html;
+ }, () => {
+ const flash = new Flash('Something went wrong on our end.');
+ return flash;
+ });
+ },
+
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
+ e.stopPropagation();
+ });
+ },
+ },
+ computed: {
+ buildsOrSpinner() {
+ return this.builds ? this.builds : this.spinner;
+ },
+ dropdownClass() {
+ if (this.builds) return 'js-builds-dropdown-container';
+ return 'js-builds-dropdown-loading builds-dropdown-loading';
+ },
+ buildStatus() {
+ return `Build: ${this.stage.status.label}`;
+ },
+ tooltip() {
+ return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
+ },
+ triggerButtonClass() {
+ return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
+ },
+ svgHTML() {
+ return borderlessStatusIconEntityMap[this.stage.status.icon];
+ },
+ },
+ watch: {
+ 'stage.title': function stageTitle() {
+ $(this.$refs.button).tooltip('destroy').tooltip();
+ },
+ },
+ template: `
+ <div>
+ <button
+ @click="fetchBuilds($event)"
+ :class="triggerButtonClass"
+ :title="stage.title"
+ data-placement="top"
+ data-toggle="dropdown"
+ type="button"
+ ref="button"
+ :aria-label="stage.title">
+ <span v-html="svgHTML" aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+ <div class="arrow-up" aria-hidden="true"></div>
+ <div
+ :class="dropdownClass"
+ class="js-builds-dropdown-list scrollable-menu"
+ v-html="buildsOrSpinner">
+ </div>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
new file mode 100644
index 00000000000..a01cb8cc202
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
@@ -0,0 +1,23 @@
+export default {
+ name: 'MRWidgetAuthor',
+ props: {
+ author: { type: Object, required: true },
+ showAuthorName: { type: Boolean, required: false, default: true },
+ showAuthorTooltip: { type: Boolean, required: false, default: false },
+ },
+ template: `
+ <a
+ :href="author.webUrl || author.web_url"
+ class="author-link"
+ :class="{ 'has-tooltip': showAuthorTooltip }"
+ :title="author.name">
+ <img
+ :src="author.avatarUrl || author.avatar_url"
+ class="avatar avatar-inline s16" />
+ <span
+ v-if="showAuthorName"
+ class="author">{{author.name}}
+ </span>
+ </a>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js
new file mode 100644
index 00000000000..6d2ed5fda64
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js
@@ -0,0 +1,27 @@
+import MRWidgetAuthor from './mr_widget_author';
+
+export default {
+ name: 'MRWidgetAuthorTime',
+ props: {
+ actionText: { type: String, required: true },
+ author: { type: Object, required: true },
+ dateTitle: { type: String, required: true },
+ dateReadable: { type: String, required: true },
+ },
+ components: {
+ 'mr-widget-author': MRWidgetAuthor,
+ },
+ template: `
+ <h4 class="js-mr-widget-author">
+ {{actionText}}
+ <mr-widget-author :author="author" />
+ <time
+ :title="dateTitle"
+ data-toggle="tooltip"
+ data-placement="top"
+ data-container="body">
+ {{dateReadable}}
+ </time>
+ </h4>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
new file mode 100644
index 00000000000..630e80a7408
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -0,0 +1,118 @@
+/* global Flash */
+
+import '~/lib/utils/datetime_utility';
+import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
+import MemoryUsage from './mr_widget_memory_usage';
+import MRWidgetService from '../services/mr_widget_service';
+
+export default {
+ name: 'MRWidgetDeployment',
+ props: {
+ mr: { type: Object, required: true },
+ service: { type: Object, required: true },
+ },
+ components: {
+ 'mr-widget-memory-usage': MemoryUsage,
+ },
+ computed: {
+ svg() {
+ return statusClassToSvgMap.icon_status_success;
+ },
+ },
+ methods: {
+ formatDate(date) {
+ return gl.utils.getTimeago().format(date);
+ },
+ hasExternalUrls(deployment = {}) {
+ return deployment.external_url && deployment.external_url_formatted;
+ },
+ hasDeploymentTime(deployment = {}) {
+ return deployment.deployed_at && deployment.deployed_at_formatted;
+ },
+ hasDeploymentMeta(deployment = {}) {
+ return deployment.url && deployment.name;
+ },
+ stopEnvironment(deployment) {
+ const msg = 'Are you sure you want to stop this environment?';
+ const isConfirmed = confirm(msg); // eslint-disable-line
+
+ if (isConfirmed) {
+ MRWidgetService.stopEnvironment(deployment.stop_url)
+ .then(res => res.json())
+ .then((res) => {
+ if (res.redirect_url) {
+ gl.utils.visitUrl(res.redirect_url);
+ }
+ })
+ .catch(() => {
+ new Flash('Something went wrong while stopping this environment. Please try again.'); // eslint-disable-line
+ });
+ }
+ },
+ },
+ template: `
+ <div class="mr-widget-heading">
+ <div v-for="deployment in mr.deployments">
+ <div class="ci-widget">
+ <div class="ci-status-icon ci-status-icon-success">
+ <span class="js-icon-link icon-link">
+ <span
+ v-html="svg"
+ aria-hidden="true"></span>
+ </span>
+ </div>
+ <span>
+ <span
+ v-if="hasDeploymentMeta(deployment)">
+ Deployed to
+ </span>
+ <a
+ v-if="hasDeploymentMeta(deployment)"
+ :href="deployment.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-meta">
+ {{deployment.name}}
+ </a>
+ <span
+ v-if="hasExternalUrls(deployment)">
+ on
+ </span>
+ <a
+ v-if="hasExternalUrls(deployment)"
+ :href="deployment.external_url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-url">
+ <i
+ class="fa fa-external-link"
+ aria-hidden="true" />
+ {{deployment.external_url_formatted}}
+ </a>
+ <span
+ v-if="hasDeploymentTime(deployment)"
+ :data-title="deployment.deployed_at_formatted"
+ class="js-deploy-time"
+ data-toggle="tooltip"
+ data-placement="top">
+ {{formatDate(deployment.deployed_at)}}
+ </span>
+ <button
+ type="button"
+ v-if="deployment.stop_url"
+ @click="stopEnvironment(deployment)"
+ class="btn btn-default btn-xs">
+ Stop environment
+ </button>
+ </span>
+ </div>
+ <mr-widget-memory-usage
+ v-if="deployment.metrics_url"
+ :mr="mr"
+ :service="service"
+ :metricsUrl="deployment.metrics_url"
+ />
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
new file mode 100644
index 00000000000..4a1fd881169
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
@@ -0,0 +1,98 @@
+require('../../lib/utils/text_utility');
+
+export default {
+ name: 'MRWidgetHeader',
+ props: {
+ mr: { type: Object, required: true },
+ },
+ computed: {
+ shouldShowCommitsBehindText() {
+ return this.mr.divergedCommitsCount > 0;
+ },
+ commitsText() {
+ return gl.text.pluralize('commit', this.mr.divergedCommitsCount);
+ },
+ },
+ methods: {
+ isBranchTitleLong(branchTitle) {
+ return branchTitle.length > 32;
+ },
+ },
+ template: `
+ <div class="mr-source-target">
+ <div
+ v-if="mr.isOpen"
+ class="pull-right">
+ <a
+ href="#modal_merge_info"
+ data-toggle="modal"
+ class="btn inline btn-grouped btn-sm">
+ Check out branch
+ </a>
+ <span class="dropdown inline prepend-left-5">
+ <a
+ class="btn btn-sm dropdown-toggle"
+ data-toggle="dropdown"
+ aria-label="Download as"
+ role="button">
+ <i
+ class="fa fa-download"
+ aria-hidden="true" />
+ <i
+ class="fa fa-caret-down"
+ aria-hidden="true" />
+ </a>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li>
+ <a
+ :href="mr.emailPatchesPath"
+ download>
+ Email patches
+ </a>
+ </li>
+ <li>
+ <a
+ :href="mr.plainDiffPath"
+ download>
+ Plain diff
+ </a>
+ </li>
+ </ul>
+ </span>
+ </div>
+ <div class="normal">
+ <b>Request to merge</b>
+ <span
+ class="label-branch"
+ :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
+ :title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
+ data-placement="bottom"
+ v-html="mr.sourceBranchLink"></span>
+ <button
+ class="btn btn-transparent btn-clipboard has-tooltip"
+ data-title="Copy branch name to clipboard"
+ :data-clipboard-text="mr.sourceBranch">
+ <i
+ aria-hidden="true"
+ class="fa fa-clipboard"></i>
+ </button>
+ <b>into</b>
+ <span
+ class="label-branch"
+ :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
+ :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
+ data-placement="bottom">
+ <a
+ :href="mr.targetBranchCommitsPath">
+ {{mr.targetBranch}}
+ </a>
+ </span>
+ <span
+ v-if="shouldShowCommitsBehindText"
+ class="diverged-commits-count">
+ ({{mr.divergedCommitsCount}} {{commitsText}} behind)
+ </span>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
new file mode 100644
index 00000000000..395cc9e91fc
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
@@ -0,0 +1,109 @@
+import statusCodes from '~/lib/utils/http_status';
+import MemoryGraph from '../../vue_shared/components/memory_graph';
+import MRWidgetService from '../services/mr_widget_service';
+
+export default {
+ name: 'MemoryUsage',
+ props: {
+ mr: { type: Object, required: true },
+ service: { type: Object, required: true },
+ metricsUrl: { type: String, required: true },
+ },
+ data() {
+ return {
+ // memoryFrom: 0,
+ // memoryTo: 0,
+ memoryMetrics: [],
+ hasMetrics: false,
+ loadFailed: false,
+ loadingMetrics: true,
+ backOffRequestCounter: 0,
+ };
+ },
+ components: {
+ 'mr-memory-graph': MemoryGraph,
+ },
+ methods: {
+ computeGraphData(metrics) {
+ this.loadingMetrics = false;
+ const { memory_values } = metrics;
+ // if (memory_previous.length > 0) {
+ // this.memoryFrom = Number(memory_previous[0].value[1]).toFixed(2);
+ // }
+ //
+ // if (memory_current.length > 0) {
+ // this.memoryTo = Number(memory_current[0].value[1]).toFixed(2);
+ // }
+
+ if (memory_values.length > 0) {
+ this.hasMetrics = true;
+ this.memoryMetrics = memory_values[0].values;
+ }
+ },
+ },
+ mounted() {
+ this.$props.loadingMetrics = true;
+ gl.utils.backOff((next, stop) => {
+ MRWidgetService.fetchMetrics(this.$props.metricsUrl)
+ .then((res) => {
+ if (res.status === statusCodes.NO_CONTENT) {
+ this.backOffRequestCounter = this.backOffRequestCounter += 1;
+ if (this.backOffRequestCounter < 3) {
+ next();
+ } else {
+ stop(res);
+ }
+ } else {
+ stop(res);
+ }
+ })
+ .catch(stop);
+ })
+ .then((res) => {
+ if (res.status === statusCodes.NO_CONTENT) {
+ return res;
+ }
+
+ return res.json();
+ })
+ .then((res) => {
+ this.computeGraphData(res.metrics);
+ return res;
+ })
+ .catch(() => {
+ this.$props.loadFailed = true;
+ });
+ },
+ template: `
+ <div class="mr-info-list mr-memory-usage">
+ <div class="legend"></div>
+ <p
+ v-if="loadingMetrics"
+ class="usage-info usage-info-loading">
+ <i
+ class="fa fa-spinner fa-spin usage-info-load-spinner"
+ aria-hidden="true" />Loading deployment statistics.
+ </p>
+ <p
+ v-if="!hasMetrics && !loadingMetrics"
+ class="usage-info usage-info-loading">
+ Deployment statistics are not available currently.
+ </p>
+ <p
+ v-if="hasMetrics"
+ class="usage-info">
+ Deployment memory usage:
+ </p>
+ <p
+ v-if="loadFailed"
+ class="usage-info">
+ Failed to load deployment statistics.
+ </p>
+ <mr-memory-graph
+ v-if="hasMetrics"
+ :metrics="memoryMetrics"
+ height="25"
+ width="100" />
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
new file mode 100644
index 00000000000..2fecebce7a0
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
@@ -0,0 +1,23 @@
+export default {
+ name: 'MRWidgetMergeHelp',
+ props: {
+ missingBranch: { type: String, required: false, default: '' },
+ },
+ template: `
+ <section class="mr-widget-help">
+ <template
+ v-if="missingBranch">
+ If the {{missingBranch}} branch exists in your local repository, you
+ </template>
+ <template v-else>
+ You
+ </template>
+ can merge this merge request manually using the
+ <a
+ data-toggle="modal"
+ href="#modal_merge_info">
+ command line.
+ </a>
+ </section>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
new file mode 100644
index 00000000000..801b9fb1ba1
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
@@ -0,0 +1,76 @@
+import PipelineStage from '../../pipelines/components/stage';
+import pipelineStatusIcon from '../../vue_shared/components/pipeline_status_icon';
+import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
+
+export default {
+ name: 'MRWidgetPipeline',
+ props: {
+ mr: { type: Object, required: true },
+ },
+ components: {
+ 'pipeline-stage': PipelineStage,
+ 'pipeline-status-icon': pipelineStatusIcon,
+ },
+ computed: {
+ hasCIError() {
+ const { hasCI, ciStatus } = this.mr;
+
+ return hasCI && !ciStatus;
+ },
+ svg() {
+ return statusClassToSvgMap.icon_status_failed;
+ },
+ stageText() {
+ return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
+ },
+ },
+ template: `
+ <div class="mr-widget-heading">
+ <div class="ci-widget">
+ <template v-if="hasCIError">
+ <div class="ci-status-icon ci-status-icon-failed js-ci-error">
+ <span class="js-icon-link icon-link">
+ <span
+ v-html="svg"
+ aria-hidden="true"></span>
+ </span>
+ </div>
+ <span>Could not connect to the CI server. Please check your settings and try again.</span>
+ </template>
+ <template v-else>
+ <pipeline-status-icon :pipelineStatus="mr.pipelineDetailedStatus" />
+ <span>
+ Pipeline
+ <a
+ :href="mr.pipeline.path"
+ class="pipeline-id">#{{mr.pipeline.id}}</a>
+ {{mr.pipeline.details.status.label}}
+ with {{stageText}}
+ </span>
+ <div class="mr-widget-pipeline-graph">
+ <div class="stage-cell">
+ <div
+ v-if="mr.pipeline.details.stages.length > 0"
+ v-for="stage in mr.pipeline.details.stages"
+ class="stage-container dropdown js-mini-pipeline-graph">
+ <pipeline-stage :stage="stage" />
+ </div>
+ </div>
+ </div>
+ <span>
+ for
+ <a
+ :href="mr.pipeline.commit.commit_path"
+ class="monospace js-commit-link">
+ {{mr.pipeline.commit.short_id}}</a>.
+ </span>
+ <span
+ v-if="mr.pipeline.coverage"
+ class="js-mr-coverage">
+ Coverage {{mr.pipeline.coverage}}%.
+ </span>
+ </template>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js
new file mode 100644
index 00000000000..205804670fa
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js
@@ -0,0 +1,42 @@
+export default {
+ name: 'MRWidgetRelatedLinks',
+ props: {
+ relatedLinks: { type: Object, required: true },
+ },
+ computed: {
+ hasLinks() {
+ const { closing, mentioned, assignToMe } = this.relatedLinks;
+ return closing || mentioned || assignToMe;
+ },
+ },
+ methods: {
+ hasMultipleIssues(text) {
+ return !text ? false : text.match(/<\/a> and <a/);
+ },
+ issueLabel(field) {
+ return this.hasMultipleIssues(this.relatedLinks[field]) ? 'issues' : 'issue';
+ },
+ verbLabel(field) {
+ return this.hasMultipleIssues(this.relatedLinks[field]) ? 'are' : 'is';
+ },
+ },
+ template: `
+ <section
+ v-if="hasLinks"
+ class="mr-info-list mr-links">
+ <div class="legend"></div>
+ <p v-if="relatedLinks.closing">
+ Closes {{issueLabel('closing')}}
+ <span v-html="relatedLinks.closing"></span>.
+ </p>
+ <p v-if="relatedLinks.mentioned">
+ <span class="capitalize">{{issueLabel('mentioned')}}</span>
+ <span v-html="relatedLinks.mentioned"></span>
+ {{verbLabel('mentioned')}} mentioned but will not be closed.
+ </p>
+ <p v-if="relatedLinks.assignToMe">
+ <span v-html="relatedLinks.assignToMe"></span>
+ </p>
+ </section>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
new file mode 100644
index 00000000000..c7f25a1697c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
@@ -0,0 +1,16 @@
+export default {
+ name: 'MRWidgetArchived',
+ template: `
+ <div class="mr-widget-body">
+ <button
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ <span class="bold">
+ This project is archived, write access has been disabled.
+ </span>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
new file mode 100644
index 00000000000..fcccb17f58d
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
@@ -0,0 +1,22 @@
+export default {
+ name: 'MRWidgetAutoMergeFailed',
+ props: {
+ mr: { type: Object, required: true },
+ },
+ template: `
+ <div class="mr-widget-body">
+ <button
+ class="btn btn-success btn-small"
+ disabled="true"
+ type="button">
+ Merge
+ </button>
+ <span class="bold danger">
+ This merge request failed to be merged automatically.
+ </span>
+ <div class="merge-error-text">
+ {{mr.mergeError}}
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
new file mode 100644
index 00000000000..8515b54e62d
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
@@ -0,0 +1,19 @@
+export default {
+ name: 'MRWidgetChecking',
+ template: `
+ <div class="mr-widget-body">
+ <button
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ <span class="bold">
+ Checking ability to merge automatically.
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ </span>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
new file mode 100644
index 00000000000..7e66441e5ff
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
@@ -0,0 +1,30 @@
+import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
+
+export default {
+ name: 'MRWidgetClosed',
+ props: {
+ mr: { type: Object, required: true },
+ },
+ components: {
+ 'mr-widget-author-and-time': mrWidgetAuthorTime,
+ },
+ template: `
+ <div class="mr-widget-body">
+ <mr-widget-author-and-time
+ actionText="Closed by"
+ :author="mr.closedBy"
+ :dateTitle="mr.updatedAt"
+ :dateReadable="mr.closedAt"
+ />
+ <section>
+ <p>
+ The changes were not merged into
+ <a
+ :href="mr.targetBranchCommitsPath"
+ class="label-branch">
+ {{mr.targetBranch}}</a>.
+ </p>
+ </section>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
new file mode 100644
index 00000000000..36596c6f37e
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
@@ -0,0 +1,39 @@
+export default {
+ name: 'MRWidgetConflicts',
+ props: {
+ mr: { type: Object, required: true },
+ },
+ template: `
+ <div class="mr-widget-body">
+ <button
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ <span class="bold">
+ There are merge conflicts.
+ <span v-if="!mr.canMerge">
+ Resolve these conflicts or ask someone with write access to this repository to merge it locally.
+ </span>
+ </span>
+ <div
+ v-if="mr.canMerge"
+ class="btn-group">
+ <a
+ v-if="mr.conflictResolutionPath"
+ :href="mr.conflictResolutionPath"
+ class="btn btn-default btn-xs js-resolve-conflicts-button">
+ Resolve conflicts
+ </a>
+ <a
+ v-if="mr.canMerge"
+ class="btn btn-default btn-xs js-merge-locally-button"
+ data-toggle="modal"
+ href="#modal_merge_info">
+ Merge locally
+ </a>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
new file mode 100644
index 00000000000..600b4d42e3d
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
@@ -0,0 +1,76 @@
+import eventHub from '../../event_hub';
+
+export default {
+ name: 'MRWidgetFailedToMerge',
+ props: {
+ mr: { type: Object, required: true },
+ },
+ data() {
+ return {
+ timer: 10,
+ isRefreshing: false,
+ };
+ },
+ mounted() {
+ setInterval(() => {
+ this.updateTimer();
+ }, 1000);
+ },
+ created() {
+ eventHub.$emit('DisablePolling');
+ },
+ computed: {
+ timerText() {
+ return this.timer > 1 ? `${this.timer} seconds` : 'a second';
+ },
+ },
+ methods: {
+ refresh() {
+ this.isRefreshing = true;
+ eventHub.$emit('MRWidgetUpdateRequested');
+ eventHub.$emit('EnablePolling');
+ },
+ updateTimer() {
+ this.timer = this.timer - 1;
+
+ if (this.timer === 0) {
+ this.refresh();
+ }
+ },
+ },
+ template: `
+ <div class="mr-widget-body">
+ <button
+ class="btn btn-success btn-small"
+ disabled="true"
+ type="button">
+ Merge
+ </button>
+ <span
+ v-if="!isRefreshing"
+ class="bold danger">
+ <span
+ class="has-error-message"
+ v-if="mr.mergeError">
+ {{mr.mergeError}}
+ </span>
+ <span v-else>Merge failed.</span>
+ <span
+ :class="{ 'has-custom-error': mr.mergeError }">
+ Refreshing in {{timerText}} to show the updated status...
+ </span>
+ <button
+ @click="refresh"
+ class="btn btn-default btn-xs js-refresh-button"
+ type="button">
+ Refresh now
+ </button>
+ </span>
+ <span
+ v-if="isRefreshing"
+ class="bold js-refresh-label">
+ Refreshing now...
+ </span>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js
new file mode 100644
index 00000000000..e3c27dfb76d
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js
@@ -0,0 +1,24 @@
+export default {
+ name: 'MRWidgetLocked',
+ props: {
+ mr: { type: Object, required: true },
+ },
+ template: `
+ <div class="mr-widget-body mr-state-locked">
+ <span class="state-label">Locked</span>
+ This merge request is in the process of being merged, during which time it is locked and cannot be closed.
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ <section class="mr-info-list mr-links">
+ <div class="legend"></div>
+ <p>
+ The changes will be merged into
+ <span class="label-branch">
+ <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
+ </span>
+ </p>
+ </section>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
new file mode 100644
index 00000000000..bcdbedcd46b
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
@@ -0,0 +1,116 @@
+/* global Flash */
+
+import MRWidgetAuthor from '../../components/mr_widget_author';
+import eventHub from '../../event_hub';
+
+export default {
+ name: 'MRWidgetMergeWhenPipelineSucceeds',
+ props: {
+ mr: { type: Object, required: true },
+ service: { type: Object, required: true },
+ },
+ components: {
+ 'mr-widget-author': MRWidgetAuthor,
+ },
+ data() {
+ return {
+ isCancellingAutoMerge: false,
+ isRemovingSourceBranch: false,
+ };
+ },
+ computed: {
+ canRemoveSourceBranch() {
+ const { shouldRemoveSourceBranch, canRemoveSourceBranch,
+ mergeUserId, currentUserId } = this.mr;
+
+ return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId;
+ },
+ },
+ methods: {
+ cancelAutomaticMerge() {
+ this.isCancellingAutoMerge = true;
+ this.service.cancelAutomaticMerge()
+ .then(res => res.json())
+ .then((res) => {
+ eventHub.$emit('UpdateWidgetData', res);
+ })
+ .catch(() => {
+ this.isCancellingAutoMerge = false;
+ new Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ });
+ },
+ removeSourceBranch() {
+ const options = {
+ sha: this.mr.sha,
+ merge_when_pipeline_succeeds: true,
+ should_remove_source_branch: true,
+ };
+
+ this.isRemovingSourceBranch = true;
+ this.service.mergeResource.save(options)
+ .then(res => res.json())
+ .then((res) => {
+ if (res.status === 'merge_when_pipeline_succeeds') {
+ eventHub.$emit('MRWidgetUpdateRequested');
+ }
+ })
+ .catch(() => {
+ this.isRemovingSourceBranch = false;
+ new Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ });
+ },
+ },
+ template: `
+ <div class="mr-widget-body">
+ <h4>
+ Set by
+ <mr-widget-author :author="mr.setToMWPSBy" />
+ to be merged automatically when the pipeline succeeds.
+ <a
+ v-if="mr.canCancelAutomaticMerge"
+ @click.prevent="cancelAutomaticMerge"
+ :disabled="isCancellingAutoMerge"
+ role="button"
+ href="#"
+ class="btn btn-xs btn-default js-cancel-auto-merge">
+ <i
+ v-if="isCancellingAutoMerge"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ Cancel automatic merge
+ </a>
+ </h4>
+ <section class="mr-info-list">
+ <div class="legend"></div>
+ <p>The changes will be merged into
+ <a
+ :href="mr.targetBranchPath"
+ class="label-branch">
+ {{mr.targetBranch}}
+ </a>
+ </p>
+ <p v-if="mr.shouldRemoveSourceBranch">
+ The source branch will be removed.
+ </p>
+ <p
+ v-else
+ class="with-button">
+ The source branch will not be removed.
+ <a
+ v-if="canRemoveSourceBranch"
+ :disabled="isRemovingSourceBranch"
+ @click.prevent="removeSourceBranch"
+ role="button"
+ class="btn btn-xs btn-default js-remove-source-branch"
+ href="#">
+ <i
+ v-if="isRemovingSourceBranch"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ Remove source branch
+ </a>
+ </p>
+ </section>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
new file mode 100644
index 00000000000..c7d32d18141
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
@@ -0,0 +1,130 @@
+/* global Flash */
+
+import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
+import eventHub from '../../event_hub';
+
+export default {
+ name: 'MRWidgetMerged',
+ props: {
+ mr: { type: Object, required: true },
+ service: { type: Object, required: true },
+ },
+ components: {
+ 'mr-widget-author-and-time': mrWidgetAuthorTime,
+ },
+ data() {
+ return {
+ isMakingRequest: false,
+ };
+ },
+ computed: {
+ shouldShowRemoveSourceBranch() {
+ const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr;
+
+ return !sourceBranchRemoved && canRemoveSourceBranch &&
+ !this.isMakingRequest && !isRemovingSourceBranch;
+ },
+ shouldShowSourceBranchRemoving() {
+ const { sourceBranchRemoved, isRemovingSourceBranch } = this.mr;
+ return !sourceBranchRemoved && (isRemovingSourceBranch || this.isMakingRequest);
+ },
+ shouldShowMergedButtons() {
+ const { canRevertInCurrentMR, canCherryPickInCurrentMR, revertInForkPath,
+ cherryPickInForkPath } = this.mr;
+
+ return canRevertInCurrentMR || canCherryPickInCurrentMR ||
+ revertInForkPath || cherryPickInForkPath;
+ },
+ },
+ methods: {
+ removeSourceBranch() {
+ this.isMakingRequest = true;
+ this.service.removeSourceBranch()
+ .then(res => res.json())
+ .then((res) => {
+ if (res.message === 'Branch was removed') {
+ eventHub.$emit('MRWidgetUpdateRequested', () => {
+ this.isMakingRequest = false;
+ });
+ }
+ })
+ .catch(() => {
+ this.isMakingRequest = false;
+ new Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ });
+ },
+ },
+ template: `
+ <div class="mr-widget-body">
+ <mr-widget-author-and-time
+ actionText="Merged by"
+ :author="mr.mergedBy"
+ :dateTitle="mr.updatedAt"
+ :dateReadable="mr.mergedAt" />
+ <section class="mr-info-list">
+ <div class="legend"></div>
+ <p>
+ The changes were merged into
+ <span class="label-branch">
+ <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
+ </span>
+ </p>
+ <p v-if="mr.sourceBranchRemoved">The source branch has been removed.</p>
+ <p v-if="shouldShowRemoveSourceBranch">
+ You can remove source branch now.
+ <button
+ @click="removeSourceBranch"
+ :class="{ disabled: isMakingRequest }"
+ type="button"
+ class="btn btn-xs btn-default js-remove-branch-button">
+ Remove Source Branch
+ </button>
+ </p>
+ <p v-if="shouldShowSourceBranchRemoving">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ The source branch is being removed.
+ </p>
+ </section>
+ <div
+ v-if="shouldShowMergedButtons"
+ class="merged-buttons clearfix">
+ <a
+ v-if="mr.canRevertInCurrentMR"
+ class="btn btn-close btn-sm has-tooltip"
+ href="#modal-revert-commit"
+ data-toggle="modal"
+ data-container="body"
+ title="Revert this merge request in a new merge request">
+ Revert
+ </a>
+ <a
+ v-else-if="mr.revertInForkPath"
+ class="btn btn-close btn-sm has-tooltip"
+ data-method="post"
+ :href="mr.revertInForkPath"
+ title="Revert this merge request in a new merge request">
+ Revert
+ </a>
+ <a
+ v-if="mr.canCherryPickInCurrentMR"
+ class="btn btn-default btn-sm has-tooltip"
+ href="#modal-cherry-pick-commit"
+ data-toggle="modal"
+ data-container="body"
+ title="Cherry-pick this merge request in a new merge request">
+ Cherry-pick
+ </a>
+ <a
+ v-else-if="mr.cherryPickInForkPath"
+ class="btn btn-default btn-sm has-tooltip"
+ data-method="post"
+ :href="mr.cherryPickInForkPath"
+ title="Cherry-pick this merge request in a new merge request">
+ Cherry-pick
+ </a>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
new file mode 100644
index 00000000000..328382485f6
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
@@ -0,0 +1,34 @@
+import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
+
+export default {
+ name: 'MRWidgetMissingBranch',
+ props: {
+ mr: { type: Object, required: true },
+ },
+ components: {
+ 'mr-widget-merge-help': mrWidgetMergeHelp,
+ },
+ computed: {
+ missingBranchName() {
+ return this.mr.sourceBranchRemoved ? 'source' : 'target';
+ },
+ },
+ template: `
+ <div class="mr-widget-body">
+ <button
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ <span class="bold js-branch-text">
+ <span class="capitalize">
+ {{missingBranchName}}
+ </span> branch does not exist.
+ Please restore the {{missingBranchName}} branch or use a different {{missingBranchName}} branch.
+ </span>
+ <mr-widget-merge-help
+ :missing-branch="missingBranchName" />
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
new file mode 100644
index 00000000000..07169b349be
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
@@ -0,0 +1,17 @@
+export default {
+ name: 'MRWidgetNotAllowed',
+ template: `
+ <div class="mr-widget-body">
+ <button
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ <span class="bold">
+ Ready to be merged automatically.
+ Ask someone with write access to this repository to merge this request.
+ </span>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
new file mode 100644
index 00000000000..8c4535f1337
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
@@ -0,0 +1,17 @@
+export default {
+ name: 'MRWidgetNothingToMerge',
+ template: `
+ <div class="mr-widget-body">
+ <button
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ <span class="bold">
+ There is nothing to merge from source branch into target branch.
+ Please push new commits or use a different branch.
+ </span>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
new file mode 100644
index 00000000000..31c53b679ed
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
@@ -0,0 +1,16 @@
+export default {
+ name: 'MRWidgetPipelineBlocked',
+ template: `
+ <div class="mr-widget-body">
+ <button
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ <span class="bold">
+ Pipeline blocked. The pipeline for this merge request requires a manual action to proceed.
+ </span>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
new file mode 100644
index 00000000000..002820123ca
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
@@ -0,0 +1,16 @@
+export default {
+ name: 'MRWidgetPipelineBlocked',
+ template: `
+ <div class="mr-widget-body">
+ <button
+ class="btn btn-success btn-small"
+ disabled="true"
+ type="button">
+ Merge
+ </button>
+ <span class="bold">
+ The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.
+ </span>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
new file mode 100644
index 00000000000..ebcc03e531b
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -0,0 +1,309 @@
+/* global Flash */
+
+import successSvg from 'icons/_icon_status_success.svg';
+import warningSvg from 'icons/_icon_status_warning.svg';
+import simplePoll from '~/lib/utils/simple_poll';
+import eventHub from '../../event_hub';
+
+export default {
+ name: 'MRWidgetReadyToMerge',
+ props: {
+ mr: { type: Object, required: true },
+ service: { type: Object, required: true },
+ },
+ data() {
+ return {
+ removeSourceBranch: true,
+ mergeWhenBuildSucceeds: false,
+ useCommitMessageWithDescription: false,
+ setToMergeWhenPipelineSucceeds: false,
+ showCommitMessageEditor: false,
+ isMakingRequest: false,
+ isMergingImmediately: false,
+ commitMessage: this.mr.commitMessage,
+ successSvg,
+ warningSvg,
+ };
+ },
+ computed: {
+ commitMessageLinkTitle() {
+ const withDesc = 'Include description in commit message';
+ const withoutDesc = "Don't include description in commit message";
+
+ return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
+ },
+ mergeButtonClass() {
+ const defaultClass = 'btn btn-success accept-merge-request';
+ const failedClass = `${defaultClass} btn-danger`;
+ const inActionClass = `${defaultClass} btn-info`;
+ const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
+
+ if (hasCI && !ciStatus) {
+ return failedClass;
+ } else if (!pipeline) {
+ return defaultClass;
+ } else if (isPipelineActive) {
+ return inActionClass;
+ } else if (isPipelineFailed) {
+ return failedClass;
+ }
+
+ return defaultClass;
+ },
+ mergeButtonText() {
+ if (this.isMergingImmediately) {
+ return 'Merge in progress';
+ } else if (this.mr.isPipelineActive) {
+ return 'Merge when pipeline succeeds';
+ }
+
+ return 'Merge';
+ },
+ shouldShowMergeOptionsDropdown() {
+ return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds;
+ },
+ isMergeButtonDisabled() {
+ const { commitMessage } = this;
+ return Boolean(!commitMessage.length
+ || !this.isMergeAllowed()
+ || this.isMakingRequest
+ || this.mr.preventMerge);
+ },
+ shouldShowSquashBeforeMerge() {
+ const { commitsCount, enableSquashBeforeMerge } = this.mr;
+ return enableSquashBeforeMerge && commitsCount > 1;
+ },
+ },
+ methods: {
+ isMergeAllowed() {
+ return !(this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.isPipelineFailed);
+ },
+ updateCommitMessage() {
+ const cmwd = this.mr.commitMessageWithDescription;
+ this.useCommitMessageWithDescription = !this.useCommitMessageWithDescription;
+ this.commitMessage = this.useCommitMessageWithDescription ? cmwd : this.mr.commitMessage;
+ },
+ toggleCommitMessageEditor() {
+ this.showCommitMessageEditor = !this.showCommitMessageEditor;
+ },
+ handleMergeButtonClick(mergeWhenBuildSucceeds, mergeImmediately) {
+ // TODO: Remove no-param-reassign
+ if (mergeWhenBuildSucceeds === undefined) {
+ mergeWhenBuildSucceeds = this.mr.isPipelineActive; // eslint-disable-line no-param-reassign
+ } else if (mergeImmediately) {
+ this.isMergingImmediately = true;
+ }
+
+ this.setToMergeWhenPipelineSucceeds = mergeWhenBuildSucceeds === true;
+
+ const options = {
+ sha: this.mr.sha,
+ commit_message: this.commitMessage,
+ merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds,
+ should_remove_source_branch: this.removeSourceBranch === true,
+ };
+
+ // Only truthy in EE extension of this component
+ if (this.setAdditionalParams) {
+ this.setAdditionalParams(options);
+ }
+
+ this.isMakingRequest = true;
+ this.service.merge(options)
+ .then(res => res.json())
+ .then((res) => {
+ const hasError = res.status === 'failed' || res.status === 'hook_validation_error';
+
+ if (res.status === 'merge_when_pipeline_succeeds') {
+ eventHub.$emit('MRWidgetUpdateRequested');
+ } else if (res.status === 'success') {
+ this.initiateMergePolling();
+ } else if (hasError) {
+ eventHub.$emit('FailedToMerge', res.merge_error);
+ }
+ })
+ .catch(() => {
+ this.isMakingRequest = false;
+ new Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ });
+ },
+ initiateMergePolling() {
+ simplePoll((continuePolling, stopPolling) => {
+ this.handleMergePolling(continuePolling, stopPolling);
+ });
+ },
+ handleMergePolling(continuePolling, stopPolling) {
+ this.service.poll()
+ .then(res => res.json())
+ .then((res) => {
+ if (res.state === 'merged') {
+ // If state is merged we should update the widget and stop the polling
+ eventHub.$emit('MRWidgetUpdateRequested');
+ eventHub.$emit('FetchActionsContent');
+ if (window.mergeRequest) {
+ window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged');
+ window.mergeRequest.decreaseCounter();
+ }
+ stopPolling();
+
+ // If user checked remove source branch and we didn't remove the branch yet
+ // we should start another polling for source branch remove process
+ if (this.removeSourceBranch && res.source_branch_exists) {
+ this.initiateRemoveSourceBranchPolling();
+ }
+ } else if (res.merge_error) {
+ eventHub.$emit('FailedToMerge', res.merge_error);
+ stopPolling();
+ } else {
+ // MR is not merged yet, continue polling until the state becomes 'merged'
+ continuePolling();
+ }
+ })
+ .catch(() => {
+ new Flash('Something went wrong while merging this merge request. Please try again.'); // eslint-disable-line
+ });
+ },
+ initiateRemoveSourceBranchPolling() {
+ // We need to show source branch is being removed spinner in another component
+ eventHub.$emit('SetBranchRemoveFlag', [true]);
+
+ simplePoll((continuePolling, stopPolling) => {
+ this.handleRemoveBranchPolling(continuePolling, stopPolling);
+ });
+ },
+ handleRemoveBranchPolling(continuePolling, stopPolling) {
+ this.service.poll()
+ .then(res => res.json())
+ .then((res) => {
+ // If source branch exists then we should continue polling
+ // because removing a source branch is a background task and takes time
+ if (res.source_branch_exists) {
+ continuePolling();
+ } else {
+ // Branch is removed. Update widget, stop polling and hide the spinner
+ eventHub.$emit('MRWidgetUpdateRequested', () => {
+ eventHub.$emit('SetBranchRemoveFlag', [false]);
+ });
+ stopPolling();
+ }
+ })
+ .catch(() => {
+ new Flash('Something went wrong while removing the source branch. Please try again.'); // eslint-disable-line
+ });
+ },
+ },
+ template: `
+ <div class="mr-widget-body">
+ <span class="btn-group">
+ <button
+ @click="handleMergeButtonClick()"
+ :disabled="isMergeButtonDisabled"
+ :class="mergeButtonClass"
+ type="button">
+ <i
+ v-if="isMakingRequest"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ {{mergeButtonText}}
+ </button>
+ <button
+ v-if="shouldShowMergeOptionsDropdown"
+ :disabled="isMergeButtonDisabled"
+ type="button"
+ class="btn btn-info dropdown-toggle"
+ data-toggle="dropdown">
+ <i
+ class="fa fa-caret-down"
+ aria-hidden="true" />
+ <span class="sr-only">
+ Select merge moment
+ </span>
+ </button>
+ <ul
+ v-if="shouldShowMergeOptionsDropdown"
+ class="dropdown-menu dropdown-menu-right"
+ role="menu">
+ <li>
+ <a
+ @click.prevent="handleMergeButtonClick(true)"
+ class="merge_when_pipeline_succeeds"
+ href="#">
+ <span
+ v-html="successSvg"
+ class="merge-opt-icon"
+ aria-hidden="true"></span>
+ <span class="merge-opt-title">Merge when pipeline succeeds</span>
+ </a>
+ </li>
+ <li>
+ <a
+ @click.prevent="handleMergeButtonClick(false, true)"
+ class="accept-merge-request"
+ href="#">
+ <span
+ v-html="warningSvg"
+ class="merge-opt-icon"
+ aria-hidden="true"></span>
+ <span class="merge-opt-title">Merge immediately</span>
+ </a>
+ </li>
+ </ul>
+ </span>
+ <template v-if="isMergeAllowed()">
+ <label class="spacing">
+ <input
+ v-model="removeSourceBranch"
+ :disabled="isMergeButtonDisabled"
+ type="checkbox"/> Remove source branch
+ </label>
+
+ <!-- Placeholder for EE extension of this component -->
+ <squash-before-merge
+ v-if="shouldShowSquashBeforeMerge"
+ :mr="mr"
+ :is-merge-button-disabled="isMergeButtonDisabled" />
+
+ <button
+ @click="toggleCommitMessageEditor"
+ :disabled="isMergeButtonDisabled"
+ class="btn btn-default btn-xs"
+ type="button">
+ Modify commit message
+ </button>
+ <div
+ v-if="showCommitMessageEditor"
+ class="prepend-top-default commit-message-editor">
+ <div class="form-group clearfix">
+ <label
+ class="control-label"
+ for="commit-message">
+ Commit message
+ </label>
+ <div class="col-sm-10">
+ <div class="commit-message-container">
+ <div class="max-width-marker"></div>
+ <textarea
+ v-model="commitMessage"
+ class="form-control js-commit-message"
+ required="required"
+ rows="14"
+ name="Commit message"></textarea>
+ </div>
+ <p class="hint">Try to keep the first line under 52 characters and the others under 72.</p>
+ <div class="hint">
+ <a
+ @click.prevent="updateCommitMessage"
+ href="#">{{commitMessageLinkTitle}}</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </template>
+ <template v-else>
+ <span class="bold">
+ The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.
+ </span>
+ </template>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
new file mode 100644
index 00000000000..bf8628d18a6
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
@@ -0,0 +1,15 @@
+/*
+The squash-before-merge button is EE only, but it's located right in the middle
+of the readyToMerge state component template.
+
+If we didn't declare this component in CE, we'd need to maintain a separate copy
+of the readyToMergeState template in EE, which is pretty big and likely to change.
+
+Instead, in CE, we declare the component, but it's hidden and is configured to do nothing.
+In EE, the configuration extends this object to add a functioning squash-before-merge
+button.
+*/
+
+export default {
+ template: '',
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
new file mode 100644
index 00000000000..f4ab2d9fa58
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
@@ -0,0 +1,27 @@
+export default {
+ name: 'MRWidgetUnresolvedDiscussions',
+ props: {
+ mr: { type: Object, required: true },
+ },
+ template: `
+ <div class="mr-widget-body">
+ <button
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ <span class="bold">
+ There are unresolved discussions. Please resolve these discussions
+ <span v-if="mr.canCreateIssue">or</span>
+ <span v-else>.</span>
+ </span>
+ <a
+ v-if="mr.createIssueToResolveDiscussionsPath"
+ :href="mr.createIssueToResolveDiscussionsPath"
+ class="btn btn-default btn-xs js-create-issue">
+ Create an issue to resolve them later
+ </a>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
new file mode 100644
index 00000000000..cb02ffe93bd
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
@@ -0,0 +1,59 @@
+/* global Flash */
+import eventHub from '../../event_hub';
+
+export default {
+ name: 'MRWidgetWIP',
+ props: {
+ mr: { type: Object, required: true },
+ service: { type: Object, required: true },
+ },
+ data() {
+ return {
+ isMakingRequest: false,
+ };
+ },
+ methods: {
+ removeWIP() {
+ this.isMakingRequest = true;
+ this.service.removeWIP()
+ .then(res => res.json())
+ .then((res) => {
+ eventHub.$emit('UpdateWidgetData', res);
+ new Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
+ $('.merge-request .detail-page-description .title').text(this.mr.title);
+ })
+ .catch(() => {
+ this.isMakingRequest = false;
+ new Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ });
+ },
+ },
+ template: `
+ <div class="mr-widget-body">
+ <button
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge</button>
+ <span class="bold">
+ This merge request is currently Work In Progress and therefore unable to merge
+ </span>
+ <template v-if="mr.removeWIPPath">
+ <i
+ class="fa fa-question-circle has-tooltip"
+ title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged." />
+ <button
+ @click="removeWIP"
+ :disabled="isMakingRequest"
+ type="button"
+ class="btn btn-default btn-xs js-remove-wip">
+ <i
+ v-if="isMakingRequest"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ Resolve WIP status
+ </button>
+ </template>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
new file mode 100644
index 00000000000..b2eb32ead5f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -0,0 +1,42 @@
+/**
+ * This file is the centerpiece of an attempt to reduce potential conflicts
+ * between the CE and EE versions of the MR widget. EE additions to the MR widget should
+ * be contained in the ./vue_merge_request_widget/ee directory, and should **extend**
+ * rather than mutate CE MR Widget code.
+ *
+ * This file should be the only source of conflicts between EE and CE. EE-only components should
+ * imported directly where they are needed, and import paths for EE extensions of CE components
+ * should overwrite import paths **without** changing the order of dependencies listed here.
+ */
+
+export { default as Vue } from 'vue';
+export { default as SmartInterval } from '~/smart_interval';
+export { default as WidgetHeader } from './components/mr_widget_header';
+export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
+export { default as WidgetPipeline } from './components/mr_widget_pipeline';
+export { default as WidgetDeployment } from './components/mr_widget_deployment';
+export { default as WidgetRelatedLinks } from './components/mr_widget_related_links';
+export { default as MergedState } from './components/states/mr_widget_merged';
+export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge';
+export { default as ClosedState } from './components/states/mr_widget_closed';
+export { default as LockedState } from './components/states/mr_widget_locked';
+export { default as WipState } from './components/states/mr_widget_wip';
+export { default as ArchivedState } from './components/states/mr_widget_archived';
+export { default as ConflictsState } from './components/states/mr_widget_conflicts';
+export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge';
+export { default as MissingBranchState } from './components/states/mr_widget_missing_branch';
+export { default as NotAllowedState } from './components/states/mr_widget_not_allowed';
+export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge';
+export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions';
+export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked';
+export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
+export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds';
+export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed';
+export { default as CheckingState } from './components/states/mr_widget_checking';
+export { default as MRWidgetStore } from './stores/mr_widget_store';
+export { default as MRWidgetService } from './services/mr_widget_service';
+export { default as eventHub } from './event_hub';
+export { default as getStateKey } from './stores/get_state_key';
+export { default as mrWidgetOptions } from './mr_widget_options';
+export { default as stateMaps } from './stores/state_maps';
+export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge';
diff --git a/app/assets/javascripts/vue_merge_request_widget/event_hub.js b/app/assets/javascripts/vue_merge_request_widget/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
new file mode 100644
index 00000000000..cd65ac069c5
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -0,0 +1,12 @@
+import {
+ Vue,
+ mrWidgetOptions,
+} from './dependencies';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const vm = new Vue(mrWidgetOptions);
+
+ window.gl.mrWidget = {
+ checkStatus: vm.checkStatus,
+ };
+});
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
new file mode 100644
index 00000000000..7c6c2d21714
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -0,0 +1,234 @@
+/* global Flash */
+
+import {
+ WidgetHeader,
+ WidgetMergeHelp,
+ WidgetPipeline,
+ WidgetDeployment,
+ WidgetRelatedLinks,
+ MergedState,
+ ClosedState,
+ LockedState,
+ WipState,
+ ArchivedState,
+ ConflictsState,
+ NothingToMergeState,
+ MissingBranchState,
+ NotAllowedState,
+ ReadyToMergeState,
+ UnresolvedDiscussionsState,
+ PipelineBlockedState,
+ PipelineFailedState,
+ FailedToMerge,
+ MergeWhenPipelineSucceedsState,
+ AutoMergeFailed,
+ CheckingState,
+ MRWidgetStore,
+ MRWidgetService,
+ eventHub,
+ stateMaps,
+ SquashBeforeMerge,
+} from './dependencies';
+
+export default {
+ el: '#js-vue-mr-widget',
+ name: 'MRWidget',
+ data() {
+ const store = new MRWidgetStore(gl.mrWidgetData);
+ const service = this.createService(store);
+ return {
+ mr: store,
+ service,
+ };
+ },
+ computed: {
+ componentName() {
+ return stateMaps.stateToComponentMap[this.mr.state];
+ },
+ shouldRenderMergeHelp() {
+ return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1;
+ },
+ shouldRenderPipelines() {
+ return Object.keys(this.mr.pipeline).length || this.mr.hasCI;
+ },
+ shouldRenderRelatedLinks() {
+ return this.mr.relatedLinks;
+ },
+ shouldRenderDeployments() {
+ return this.mr.deployments.length;
+ },
+ },
+ methods: {
+ createService(store) {
+ const endpoints = {
+ mergePath: store.mergePath,
+ mergeCheckPath: store.mergeCheckPath,
+ cancelAutoMergePath: store.cancelAutoMergePath,
+ removeWIPPath: store.removeWIPPath,
+ sourceBranchPath: store.sourceBranchPath,
+ ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath,
+ statusPath: store.statusPath,
+ mergeActionsContentPath: store.mergeActionsContentPath,
+ };
+ return new MRWidgetService(endpoints);
+ },
+ checkStatus(cb) {
+ this.service.checkStatus()
+ .then(res => res.json())
+ .then((res) => {
+ this.mr.setData(res);
+ this.setFavicon();
+ if (cb) {
+ cb.call(null, res);
+ }
+ })
+ .catch(() => {
+ new Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ });
+ },
+ initPolling() {
+ this.pollingInterval = new gl.SmartInterval({
+ callback: this.checkStatus,
+ startingInterval: 10000,
+ maxInterval: 30000,
+ hiddenInterval: 120000,
+ incrementByFactorOf: 5000,
+ });
+ },
+ initDeploymentsPolling() {
+ this.deploymentsInterval = new gl.SmartInterval({
+ callback: this.fetchDeployments,
+ startingInterval: 30000,
+ maxInterval: 120000,
+ hiddenInterval: 240000,
+ incrementByFactorOf: 15000,
+ immediateExecution: true,
+ });
+ },
+ setFavicon() {
+ if (this.mr.ciStatusFaviconPath) {
+ gl.utils.setFavicon(this.mr.ciStatusFaviconPath);
+ }
+ },
+ fetchDeployments() {
+ this.service.fetchDeployments()
+ .then(res => res.json())
+ .then((res) => {
+ if (res.length) {
+ this.mr.deployments = res;
+ }
+ })
+ .catch(() => {
+ new Flash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line
+ });
+ },
+ fetchActionsContent() {
+ this.service.fetchMergeActionsContent()
+ .then((res) => {
+ if (res.body) {
+ const el = document.createElement('div');
+ el.innerHTML = res.body;
+ document.body.appendChild(el);
+ }
+ })
+ .catch(() => {
+ new Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ });
+ },
+ resumePolling() {
+ this.pollingInterval.resume();
+ },
+ stopPolling() {
+ this.pollingInterval.stopTimer();
+ },
+ bindEventHubListeners() {
+ eventHub.$on('MRWidgetUpdateRequested', (cb) => {
+ this.checkStatus(cb);
+ });
+
+ // `params` should be an Array contains a Boolean, like `[true]`
+ // Passing parameter as Boolean didn't work.
+ eventHub.$on('SetBranchRemoveFlag', (params) => {
+ this.mr.isRemovingSourceBranch = params[0];
+ });
+
+ eventHub.$on('FailedToMerge', (mergeError) => {
+ this.mr.state = 'failedToMerge';
+ this.mr.mergeError = mergeError;
+ });
+
+ eventHub.$on('UpdateWidgetData', (data) => {
+ this.mr.setData(data);
+ });
+
+ eventHub.$on('FetchActionsContent', () => {
+ this.fetchActionsContent();
+ });
+
+ eventHub.$on('EnablePolling', () => {
+ this.resumePolling();
+ });
+
+ eventHub.$on('DisablePolling', () => {
+ this.stopPolling();
+ });
+ },
+ handleMounted() {
+ this.checkStatus();
+ this.setFavicon();
+ this.initDeploymentsPolling();
+ },
+ },
+ created() {
+ this.initPolling();
+ this.bindEventHubListeners();
+ },
+ mounted() {
+ this.handleMounted();
+ },
+ components: {
+ 'mr-widget-header': WidgetHeader,
+ 'mr-widget-merge-help': WidgetMergeHelp,
+ 'mr-widget-pipeline': WidgetPipeline,
+ 'mr-widget-deployment': WidgetDeployment,
+ 'mr-widget-related-links': WidgetRelatedLinks,
+ 'mr-widget-merged': MergedState,
+ 'mr-widget-closed': ClosedState,
+ 'mr-widget-locked': LockedState,
+ 'mr-widget-failed-to-merge': FailedToMerge,
+ 'mr-widget-wip': WipState,
+ 'mr-widget-archived': ArchivedState,
+ 'mr-widget-conflicts': ConflictsState,
+ 'mr-widget-nothing-to-merge': NothingToMergeState,
+ 'mr-widget-not-allowed': NotAllowedState,
+ 'mr-widget-missing-branch': MissingBranchState,
+ 'mr-widget-ready-to-merge': ReadyToMergeState,
+ 'mr-widget-squash-before-merge': SquashBeforeMerge,
+ 'mr-widget-checking': CheckingState,
+ 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
+ 'mr-widget-pipeline-blocked': PipelineBlockedState,
+ 'mr-widget-pipeline-failed': PipelineFailedState,
+ 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
+ 'mr-widget-auto-merge-failed': AutoMergeFailed,
+ },
+ template: `
+ <div class="mr-state-widget prepend-top-default">
+ <mr-widget-header :mr="mr" />
+ <mr-widget-pipeline
+ v-if="shouldRenderPipelines"
+ :mr="mr" />
+ <mr-widget-deployment
+ v-if="shouldRenderDeployments"
+ :mr="mr"
+ :service="service" />
+ <component
+ :is="componentName"
+ :mr="mr"
+ :service="service" />
+ <mr-widget-related-links
+ v-if="shouldRenderRelatedLinks"
+ :related-links="mr.relatedLinks" />
+ <mr-widget-merge-help v-if="shouldRenderMergeHelp" />
+ </div>
+ `,
+};
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
new file mode 100644
index 00000000000..42493be3372
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -0,0 +1,57 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class MRWidgetService {
+ constructor(endpoints) {
+ this.mergeResource = Vue.resource(endpoints.mergePath);
+ this.mergeCheckResource = Vue.resource(endpoints.mergeCheckPath);
+ this.cancelAutoMergeResource = Vue.resource(endpoints.cancelAutoMergePath);
+ this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
+ this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
+ this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
+ this.pollResource = Vue.resource(`${endpoints.statusPath}?basic=true`);
+ this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
+ }
+
+ merge(data) {
+ return this.mergeResource.save(data);
+ }
+
+ cancelAutomaticMerge() {
+ return this.cancelAutoMergeResource.save();
+ }
+
+ removeWIP() {
+ return this.removeWIPResource.save();
+ }
+
+ removeSourceBranch() {
+ return this.removeSourceBranchResource.delete();
+ }
+
+ fetchDeployments() {
+ return this.deploymentsResource.get();
+ }
+
+ poll() {
+ return this.pollResource.get();
+ }
+
+ checkStatus() {
+ return this.mergeCheckResource.get();
+ }
+
+ fetchMergeActionsContent() {
+ return this.mergeActionsContentResource.get();
+ }
+
+ static stopEnvironment(url) {
+ return Vue.http.post(url);
+ }
+
+ static fetchMetrics(metricsUrl) {
+ return Vue.http.get(`${metricsUrl}.json`);
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
new file mode 100644
index 00000000000..fee4113f3c8
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -0,0 +1,28 @@
+export default function deviseState(data) {
+ if (data.project_archived) {
+ return 'archived';
+ } else if (data.branch_missing) {
+ return 'missingBranch';
+ } else if (!data.commits_count) {
+ return 'nothingToMerge';
+ } else if (this.mergeStatus === 'unchecked') {
+ return 'checking';
+ } else if (data.has_conflicts) {
+ return 'conflicts';
+ } else if (data.work_in_progress) {
+ return 'workInProgress';
+ } else if (this.mergeWhenPipelineSucceeds) {
+ return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds';
+ } else if (!this.canMerge) {
+ return 'notAllowedToMerge';
+ } else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
+ return 'pipelineFailed';
+ } else if (this.hasMergeableDiscussionsState) {
+ return 'unresolvedDiscussions';
+ } else if (this.isPipelineBlocked) {
+ return 'pipelineBlocked';
+ } else if (this.canBeMerged) {
+ return 'readyToMerge';
+ }
+ return null;
+}
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
new file mode 100644
index 00000000000..faafeae5c5b
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -0,0 +1,134 @@
+import Timeago from 'timeago.js';
+import { getStateKey } from '../dependencies';
+
+export default class MergeRequestStore {
+
+ constructor(data) {
+ this.setData(data);
+ }
+
+ setData(data) {
+ const currentUser = data.current_user;
+ const pipelineStatus = data.pipeline ? data.pipeline.details.status : null;
+
+ this.title = data.title;
+ this.targetBranch = data.target_branch;
+ this.sourceBranch = data.source_branch;
+ this.mergeStatus = data.merge_status;
+ this.sha = data.diff_head_sha;
+ this.commitMessage = data.merge_commit_message;
+ this.commitMessageWithDescription = data.merge_commit_message_with_description;
+ this.commitsCount = data.commits_count;
+ this.divergedCommitsCount = data.diverged_commits_count;
+ this.pipeline = data.pipeline || {};
+ this.deployments = this.deployments || data.deployments || [];
+
+ if (data.issues_links) {
+ const links = data.issues_links;
+ const { closing } = links;
+ const mentioned = links.mentioned_but_not_closing;
+ const assignToMe = links.assign_to_closing;
+
+ if (closing || mentioned || assignToMe) {
+ this.relatedLinks = { closing, mentioned, assignToMe };
+ }
+ }
+
+ this.updatedAt = data.updated_at;
+ this.mergedAt = MergeRequestStore.getEventDate(data.merge_event);
+ this.closedAt = MergeRequestStore.getEventDate(data.closed_event);
+ this.mergedBy = MergeRequestStore.getAuthorObject(data.merge_event);
+ this.closedBy = MergeRequestStore.getAuthorObject(data.closed_event);
+ this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} });
+ this.mergeUserId = data.merge_user_id;
+ this.currentUserId = gon.current_user_id;
+ this.sourceBranchPath = data.source_branch_path;
+ this.sourceBranchLink = data.source_branch_with_namespace_link;
+ this.mergeError = data.merge_error;
+ this.targetBranchPath = data.target_branch_commits_path;
+ this.conflictResolutionPath = data.conflict_resolution_path;
+ this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path;
+ this.removeWIPPath = data.remove_wip_path;
+ this.sourceBranchRemoved = !data.source_branch_exists;
+ this.shouldRemoveSourceBranch = (data.merge_params || {}).should_remove_source_branch || false;
+ this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
+ this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
+ this.mergePath = data.merge_path;
+ this.statusPath = data.status_path;
+ this.emailPatchesPath = data.email_patches_path;
+ this.plainDiffPath = data.plain_diff_path;
+ this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
+ this.mergeCheckPath = data.merge_check_path;
+ this.mergeActionsContentPath = data.commit_change_content_path;
+ this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
+ this.isOpen = data.state === 'opened' || data.state === 'reopened' || false;
+ this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
+ this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
+ this.canMerge = !!data.merge_path;
+ this.canCreateIssue = currentUser.can_create_issue || false;
+ this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
+ this.canBeMerged = data.can_be_merged || false;
+
+ // Cherry-pick and Revert actions related
+ this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
+ this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false;
+ this.cherryPickInForkPath = currentUser.cherry_pick_in_fork_path;
+ this.revertInForkPath = currentUser.revert_in_fork_path;
+
+ // CI related
+ this.ciEnvironmentsStatusPath = data.ci_environments_status_path;
+ this.hasCI = data.has_ci;
+ this.ciStatus = data.ci_status;
+ this.isPipelineFailed = this.ciStatus ? (this.ciStatus === 'failed' || this.ciStatus === 'canceled') : false;
+ this.pipelineDetailedStatus = pipelineStatus;
+ this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
+ this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
+ this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
+
+ this.setState(data);
+ }
+
+ setState(data) {
+ if (this.isOpen) {
+ this.state = getStateKey.call(this, data);
+ } else {
+ switch (data.state) {
+ case 'merged':
+ this.state = 'merged';
+ break;
+ case 'closed':
+ this.state = 'closed';
+ break;
+ case 'locked':
+ this.state = 'locked';
+ break;
+ default:
+ this.state = null;
+ }
+ }
+ }
+
+ static getAuthorObject(event) {
+ if (!event) {
+ return {};
+ }
+
+ return {
+ name: event.author.name || '',
+ username: event.author.username || '',
+ webUrl: event.author.web_url || '',
+ avatarUrl: event.author.avatar_url || '',
+ };
+ }
+
+ static getEventDate(event) {
+ const timeagoInstance = new Timeago();
+
+ if (!event) {
+ return '';
+ }
+
+ return timeagoInstance.format(event.updated_at);
+ }
+
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
new file mode 100644
index 00000000000..625d7a01c65
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -0,0 +1,36 @@
+const stateToComponentMap = {
+ merged: 'mr-widget-merged',
+ closed: 'mr-widget-closed',
+ locked: 'mr-widget-locked',
+ conflicts: 'mr-widget-conflicts',
+ missingBranch: 'mr-widget-missing-branch',
+ workInProgress: 'mr-widget-wip',
+ readyToMerge: 'mr-widget-ready-to-merge',
+ nothingToMerge: 'mr-widget-nothing-to-merge',
+ notAllowedToMerge: 'mr-widget-not-allowed',
+ archived: 'mr-widget-archived',
+ checking: 'mr-widget-checking',
+ unresolvedDiscussions: 'mr-widget-unresolved-discussions',
+ pipelineBlocked: 'mr-widget-pipeline-blocked',
+ pipelineFailed: 'mr-widget-pipeline-failed',
+ mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds',
+ failedToMerge: 'mr-widget-failed-to-merge',
+ autoMergeFailed: 'mr-widget-auto-merge-failed',
+};
+
+const statesToShowHelpWidget = [
+ 'locked',
+ 'conflicts',
+ 'workInProgress',
+ 'readyToMerge',
+ 'checking',
+ 'unresolvedDiscussions',
+ 'pipelineFailed',
+ 'pipelineBlocked',
+ 'autoMergeFailed',
+];
+
+export default {
+ stateToComponentMap,
+ statesToShowHelpWidget,
+};
diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.js b/app/assets/javascripts/vue_shared/components/memory_graph.js
new file mode 100644
index 00000000000..2a605b24339
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/memory_graph.js
@@ -0,0 +1,36 @@
+export default {
+ name: 'MemoryGraph',
+ props: {
+ metrics: { type: Array, required: true },
+ width: { type: String, required: true },
+ height: { type: String, required: true },
+ },
+ data() {
+ return {
+ pathD: '',
+ pathViewBox: '',
+ // dotX: '',
+ // dotY: '',
+ };
+ },
+ mounted() {
+ const renderData = this.$props.metrics.map(v => v[1]);
+ const maxMemory = Math.max.apply(null, renderData);
+ const minMemory = Math.min.apply(null, renderData);
+ const diff = maxMemory - minMemory;
+ // const cx = 0;
+ // const cy = 0;
+ const lineWidth = renderData.length;
+ const linePath = renderData.map((y, x) => `${x} ${maxMemory - y}`);
+ this.pathD = `M ${linePath}`;
+ this.pathViewBox = `0 0 ${lineWidth} ${diff}`;
+ },
+ template: `
+ <div class="memory-graph-container">
+ <svg :width="width" :height="height" xmlns="http://www.w3.org/2000/svg">
+ <path :d="pathD" :viewBox="pathViewBox" />
+ <!--<circle r="0.8" :cx="dotX" :cy="dotY" tranform="translate(0 -1)" /> -->
+ </svg>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipeline_status_icon.js b/app/assets/javascripts/vue_shared/components/pipeline_status_icon.js
new file mode 100644
index 00000000000..ae246ada01b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipeline_status_icon.js
@@ -0,0 +1,23 @@
+import { statusClassToSvgMap } from '../pipeline_svg_icons';
+
+export default {
+ name: 'PipelineStatusIcon',
+ props: {
+ pipelineStatus: { type: Object, required: true, default: () => ({}) },
+ },
+ computed: {
+ svg() {
+ return statusClassToSvgMap[this.pipelineStatus.icon];
+ },
+ statusClass() {
+ return `ci-status-icon ci-status-icon-${this.pipelineStatus.group}`;
+ },
+ },
+ template: `
+ <div :class="statusClass">
+ <a class="icon-link" :href="pipelineStatus.details_path">
+ <span v-html="svg" aria-hidden="true"></span>
+ </a>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/pipeline_svg_icons.js b/app/assets/javascripts/vue_shared/pipeline_svg_icons.js
new file mode 100644
index 00000000000..5af30ae74f0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/pipeline_svg_icons.js
@@ -0,0 +1,43 @@
+import canceledSvg from 'icons/_icon_status_canceled.svg';
+import createdSvg from 'icons/_icon_status_created.svg';
+import failedSvg from 'icons/_icon_status_failed.svg';
+import manualSvg from 'icons/_icon_status_manual.svg';
+import pendingSvg from 'icons/_icon_status_pending.svg';
+import runningSvg from 'icons/_icon_status_running.svg';
+import skippedSvg from 'icons/_icon_status_skipped.svg';
+import successSvg from 'icons/_icon_status_success.svg';
+import warningSvg from 'icons/_icon_status_warning.svg';
+
+import canceledBorderlessSvg from 'icons/_icon_status_canceled_borderless.svg';
+import createdBorderlessSvg from 'icons/_icon_status_created_borderless.svg';
+import failedBorderlessSvg from 'icons/_icon_status_failed_borderless.svg';
+import manualBorderlessSvg from 'icons/_icon_status_manual_borderless.svg';
+import pendingBorderlessSvg from 'icons/_icon_status_pending_borderless.svg';
+import runningBorderlessSvg from 'icons/_icon_status_running_borderless.svg';
+import skippedBorderlessSvg from 'icons/_icon_status_skipped_borderless.svg';
+import successBorderlessSvg from 'icons/_icon_status_success_borderless.svg';
+import warningBorderlessSvg from 'icons/_icon_status_warning_borderless.svg';
+
+export const statusClassToSvgMap = {
+ icon_status_canceled: canceledSvg,
+ icon_status_created: createdSvg,
+ icon_status_failed: failedSvg,
+ icon_status_manual: manualSvg,
+ icon_status_pending: pendingSvg,
+ icon_status_running: runningSvg,
+ icon_status_skipped: skippedSvg,
+ icon_status_success: successSvg,
+ icon_status_warning: warningSvg,
+};
+
+export const statusClassToBorderlessSvgMap = {
+ icon_status_canceled: canceledBorderlessSvg,
+ icon_status_created: createdBorderlessSvg,
+ icon_status_failed: failedBorderlessSvg,
+ icon_status_manual: manualBorderlessSvg,
+ icon_status_pending: pendingBorderlessSvg,
+ icon_status_running: runningBorderlessSvg,
+ icon_status_skipped: skippedBorderlessSvg,
+ icon_status_success: successBorderlessSvg,
+ icon_status_warning: warningBorderlessSvg,
+};