summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/api.js7
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js19
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js33
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js15
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js42
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js53
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue192
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue3
-rw-r--r--app/assets/javascripts/clusters/constants.js9
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js16
-rw-r--r--app/assets/javascripts/commons/jquery.js1
-rw-r--r--app/assets/javascripts/diffs/components/app.vue4
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue18
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue3
-rw-r--r--app/assets/javascripts/diffs/components/diff_stats.vue52
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue68
-rw-r--r--app/assets/javascripts/diffs/index.js51
-rw-r--r--app/assets/javascripts/diffs/store/actions.js4
-rw-r--r--app/assets/javascripts/diffs/store/getters.js37
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js3
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js3
-rw-r--r--app/assets/javascripts/due_date_select.js2
-rw-r--r--app/assets/javascripts/environments/components/container.vue10
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue10
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue5
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue8
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue5
-rw-r--r--app/assets/javascripts/environments/index.js2
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js2
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js4
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js3
-rw-r--r--app/assets/javascripts/groups_select.js172
-rw-r--r--app/assets/javascripts/ide/components/ide.vue46
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue13
-rw-r--r--app/assets/javascripts/ide/constants.js5
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js2
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js12
-rw-r--r--app/assets/javascripts/issuable_context.js12
-rw-r--r--app/assets/javascripts/issuable_form.js63
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue59
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue24
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue6
-rw-r--r--app/assets/javascripts/issue_show/stores/index.js11
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue4
-rw-r--r--app/assets/javascripts/label_manager.js13
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js42
-rw-r--r--app/assets/javascripts/lib/utils/file_upload.js3
-rw-r--r--app/assets/javascripts/lib/utils/grammar.js40
-rw-r--r--app/assets/javascripts/lib/utils/icon_utils.js18
-rw-r--r--app/assets/javascripts/main.js33
-rw-r--r--app/assets/javascripts/member_expiration_date.js1
-rw-r--r--app/assets/javascripts/merge_request.js9
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue153
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue89
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue329
-rw-r--r--app/assets/javascripts/monitoring/components/graph/axis.vue118
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue48
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue151
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue62
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue65
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_info.vue28
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_line.vue33
-rw-r--r--app/assets/javascripts/monitoring/event_hub.js3
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js86
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js14
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js42
-rw-r--r--app/assets/javascripts/monitoring/utils/measurements.js44
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js223
-rw-r--r--app/assets/javascripts/notes.js4
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/discussion_reply_placeholder.vue17
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue29
-rw-r--r--app/assets/javascripts/notes/components/note_actions/reply_button.vue40
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue1
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue12
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue22
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue18
-rw-r--r--app/assets/javascripts/notes/index.js3
-rw-r--r--app/assets/javascripts/notes/stores/actions.js3
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js5
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index.js2
-rw-r--r--app/assets/javascripts/pages/explore/projects/index.js2
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js12
-rw-r--r--app/assets/javascripts/pages/projects/environments/metrics/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js18
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js10
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue2
-rw-r--r--app/assets/javascripts/project_select.js174
-rw-r--r--app/assets/javascripts/project_select_combo_button.js10
-rw-r--r--app/assets/javascripts/projects/project_new.js26
-rw-r--r--app/assets/javascripts/serverless/components/environment_row.vue65
-rw-r--r--app/assets/javascripts/serverless/components/function_details.vue25
-rw-r--r--app/assets/javascripts/serverless/components/function_row.vue55
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue59
-rw-r--r--app/assets/javascripts/serverless/components/url.vue38
-rw-r--r--app/assets/javascripts/serverless/stores/serverless_store.js11
-rw-r--r--app/assets/javascripts/task_list.js72
-rw-r--r--app/assets/javascripts/users_select.js190
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue40
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue38
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue91
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue278
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue (renamed from app/assets/javascripts/ide/components/file_finder/index.vue)136
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/item.vue (renamed from app/assets/javascripts/ide/components/file_finder/item.vue)26
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue8
-rw-r--r--app/assets/stylesheets/framework/common.scss24
-rw-r--r--app/assets/stylesheets/framework/files.scss20
-rw-r--r--app/assets/stylesheets/framework/images.scss3
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss25
-rw-r--r--app/assets/stylesheets/pages/clusters.scss14
-rw-r--r--app/assets/stylesheets/pages/diff.scss52
-rw-r--r--app/assets/stylesheets/pages/environments.scss12
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss49
-rw-r--r--app/assets/stylesheets/pages/projects.scss26
-rw-r--r--app/assets/stylesheets/pages/serverless.scss3
-rw-r--r--app/assets/stylesheets/pages/settings.scss2
135 files changed, 2255 insertions, 2371 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index d1396b6c4bc..85eb08cc97d 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -5,6 +5,7 @@ import axios from './lib/utils/axios_utils';
const Api = {
groupsPath: '/api/:version/groups.json',
groupPath: '/api/:version/groups/:id',
+ groupMembersPath: '/api/:version/groups/:id/members',
subgroupsPath: '/api/:version/groups/:id/subgroups',
namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json',
@@ -40,6 +41,12 @@ const Api = {
});
},
+ groupMembers(id) {
+ const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id));
+
+ return axios.get(url);
+ },
+
// Return groups list. Filtered by query
groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index cace8bb9dba..73ce3e760ab 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -437,7 +437,7 @@ export class AwardsHandler {
createAwardButtonForVotesBlock(votesBlock, emojiName) {
const buttonHtml = `
- <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
+ <button class="btn award-control js-emoji-btn has-tooltip active" title="You">
${this.emoji.glEmojiTag(emojiName)}
<span class="award-control-text js-counter">1</span>
</button>
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
index 56293d5f96f..d1d75658181 100644
--- a/app/assets/javascripts/behaviors/gl_emoji.js
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -1,11 +1,10 @@
-import installCustomElements from 'document-register-element';
+import 'document-register-element';
import isEmojiUnicodeSupported from '../emoji/support';
-installCustomElements(window);
+class GlEmoji extends HTMLElement {
+ constructor() {
+ super();
-export default function installGlEmojiElement() {
- const GlEmojiElementProto = Object.create(HTMLElement.prototype);
- GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim();
const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset;
@@ -43,9 +42,11 @@ export default function installGlEmojiElement() {
});
}
}
- };
+ }
+}
- document.registerElement('gl-emoji', {
- prototype: GlEmojiElementProto,
- });
+export default function installGlEmojiElement() {
+ if (!customElements.get('gl-emoji')) {
+ customElements.define('gl-emoji', GlEmoji);
+ }
}
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index 947d019c725..52d9f2f0322 100644
--- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -1,8 +1,5 @@
import $ from 'jquery';
-import { DOMParser } from 'prosemirror-model';
import { getSelectedFragment } from '~/lib/utils/common_utils';
-import schema from './schema';
-import markdownSerializer from './serializer';
export class CopyAsGFM {
constructor() {
@@ -39,9 +36,13 @@ export class CopyAsGFM {
div.appendChild(el.cloneNode(true));
const html = div.innerHTML;
- clipboardData.setData('text/plain', el.textContent);
- clipboardData.setData('text/x-gfm', this.nodeToGFM(el));
- clipboardData.setData('text/html', html);
+ CopyAsGFM.nodeToGFM(el)
+ .then(res => {
+ clipboardData.setData('text/plain', el.textContent);
+ clipboardData.setData('text/x-gfm', res);
+ clipboardData.setData('text/html', html);
+ })
+ .catch(() => {});
}
static pasteGFM(e) {
@@ -137,11 +138,21 @@ export class CopyAsGFM {
}
static nodeToGFM(node) {
- const wrapEl = document.createElement('div');
- wrapEl.appendChild(node.cloneNode(true));
- const doc = DOMParser.fromSchema(schema).parse(wrapEl);
-
- return markdownSerializer.serialize(doc);
+ return Promise.all([
+ import(/* webpackChunkName: 'gfm_copy_extra' */ 'prosemirror-model'),
+ import(/* webpackChunkName: 'gfm_copy_extra' */ './schema'),
+ import(/* webpackChunkName: 'gfm_copy_extra' */ './serializer'),
+ ])
+ .then(([prosemirrorModel, schema, markdownSerializer]) => {
+ const { DOMParser } = prosemirrorModel;
+ const wrapEl = document.createElement('div');
+ wrapEl.appendChild(node.cloneNode(true));
+ const doc = DOMParser.fromSchema(schema.default).parse(wrapEl);
+
+ const res = markdownSerializer.default.serialize(doc);
+ return res;
+ })
+ .catch(() => {});
}
}
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 35f1bb6b080..7adccbb062f 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -28,16 +28,13 @@ MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function($form) {
var mdText;
- var markdownVersion;
- var url;
var preview = $form.find('.js-md-preview');
+ var url = preview.data('url');
if (preview.hasClass('md-preview-loading')) {
return;
}
mdText = $form.find('textarea.markdown-area').val();
- markdownVersion = $form.attr('data-markdown-version');
- url = this.versionedPreviewPath(preview.data('url'), markdownVersion);
if (mdText.trim().length === 0) {
preview.text(this.emptyMessage);
@@ -67,16 +64,6 @@ MarkdownPreview.prototype.showPreview = function($form) {
}
};
-MarkdownPreview.prototype.versionedPreviewPath = function(markdownPreviewPath, markdownVersion) {
- if (typeof markdownVersion === 'undefined') {
- return markdownPreviewPath;
- }
-
- return `${markdownPreviewPath}${
- markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
- }markdown_version=${markdownVersion}`;
-};
-
MarkdownPreview.prototype.fetchMarkdownPreview = function(text, url, success) {
if (!url) {
return;
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 0eb067d4963..680f2031409 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -64,26 +64,30 @@ export default class ShortcutsIssuable extends Shortcuts {
const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
const blockquoteEl = document.createElement('blockquote');
blockquoteEl.appendChild(el);
- const text = CopyAsGFM.nodeToGFM(blockquoteEl);
-
- if (text.trim() === '') {
- return false;
- }
-
- // If replyField already has some content, add a newline before our quote
- const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
- $replyField
- .val((a, current) => `${current}${separator}${text}\n\n`)
- .trigger('input')
- .trigger('change');
-
- // Trigger autosize
- const event = document.createEvent('Event');
- event.initEvent('autosize:update', true, false);
- $replyField.get(0).dispatchEvent(event);
+ CopyAsGFM.nodeToGFM(blockquoteEl)
+ .then(text => {
+ if (text.trim() === '') {
+ return false;
+ }
+
+ // If replyField already has some content, add a newline before our quote
+ const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
+ $replyField
+ .val((a, current) => `${current}${separator}${text}\n\n`)
+ .trigger('input')
+ .trigger('change');
+
+ // Trigger autosize
+ const event = document.createEvent('Event');
+ event.initEvent('autosize:update', true, false);
+ $replyField.get(0).dispatchEvent(event);
+
+ // Focus the input field
+ $replyField.focus();
- // Focus the input field
- $replyField.focus();
+ return false;
+ })
+ .catch(() => {});
return false;
}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index b1f992c03ff..6ebd1ad109e 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -6,7 +6,13 @@ import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
-import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
+import {
+ APPLICATION_STATUS,
+ REQUEST_SUBMITTED,
+ REQUEST_FAILURE,
+ UPGRADE_REQUESTED,
+ UPGRADE_REQUEST_FAILURE,
+} from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
@@ -120,11 +126,17 @@ export default class Clusters {
addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
+ eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
+ eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId));
+ eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
}
removeListeners() {
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication);
+ eventHub.$off('upgradeApplication', this.upgradeApplication);
+ eventHub.$off('upgradeFailed', this.upgradeFailed);
+ eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
}
initPolling() {
@@ -231,22 +243,33 @@ export default class Clusters {
installApplication(data) {
const appId = data.id;
- this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUBMITTED);
this.store.updateAppProperty(appId, 'requestReason', null);
+ this.store.updateAppProperty(appId, 'statusReason', null);
+
+ this.service.installApplication(appId, data.params).catch(() => {
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
+ this.store.updateAppProperty(
+ appId,
+ 'requestReason',
+ s__('ClusterIntegration|Request to begin installing failed'),
+ );
+ });
+ }
+
+ upgradeApplication(data) {
+ const appId = data.id;
+ this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUESTED);
+ this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING);
+ this.service.installApplication(appId, data.params).catch(() => this.upgradeFailed(appId));
+ }
+
+ upgradeFailed(appId) {
+ this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUEST_FAILURE);
+ }
- this.service
- .installApplication(appId, data.params)
- .then(() => {
- this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
- })
- .catch(() => {
- this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
- this.store.updateAppProperty(
- appId,
- 'requestReason',
- s__('ClusterIntegration|Request to begin installing failed'),
- );
- });
+ dismissUpgradeSuccess(appId) {
+ this.store.updateAppProperty(appId, 'requestStatus', null);
}
destroy() {
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index d4354dcfebd..5952e93b9a7 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -1,20 +1,24 @@
<script>
/* eslint-disable vue/require-default-prop */
+import { GlLink } from '@gitlab/ui';
+import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import {
APPLICATION_STATUS,
- REQUEST_LOADING,
- REQUEST_SUCCESS,
+ REQUEST_SUBMITTED,
REQUEST_FAILURE,
+ UPGRADE_REQUESTED,
} from '../constants';
export default {
components: {
loadingButton,
identicon,
+ TimeagoTooltip,
+ GlLink,
},
props: {
id: {
@@ -59,6 +63,18 @@ export default {
type: String,
required: false,
},
+ version: {
+ type: String,
+ required: false,
+ },
+ chartRepo: {
+ type: String,
+ required: false,
+ },
+ upgradeAvailable: {
+ type: Boolean,
+ required: false,
+ },
installApplicationRequestParams: {
type: Object,
required: false,
@@ -72,11 +88,31 @@ export default {
isKnownStatus() {
return Object.values(APPLICATION_STATUS).includes(this.status);
},
+ isInstalling() {
+ return (
+ this.status === APPLICATION_STATUS.SCHEDULED ||
+ this.status === APPLICATION_STATUS.INSTALLING ||
+ (this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.isInstalled)
+ );
+ },
isInstalled() {
return (
this.status === APPLICATION_STATUS.INSTALLED ||
this.status === APPLICATION_STATUS.UPDATED ||
- this.status === APPLICATION_STATUS.UPDATING
+ this.status === APPLICATION_STATUS.UPDATING ||
+ this.status === APPLICATION_STATUS.UPDATE_ERRORED
+ );
+ },
+ canInstall() {
+ if (this.isInstalling) {
+ return false;
+ }
+
+ return (
+ this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
+ this.status === APPLICATION_STATUS.INSTALLABLE ||
+ this.status === APPLICATION_STATUS.ERROR ||
+ this.isUnknownStatus
);
},
hasLogo() {
@@ -90,12 +126,7 @@ export default {
return `js-cluster-application-row-${this.id}`;
},
installButtonLoading() {
- return (
- !this.status ||
- this.status === APPLICATION_STATUS.SCHEDULED ||
- this.status === APPLICATION_STATUS.INSTALLING ||
- this.requestStatus === REQUEST_LOADING
- );
+ return !this.status || this.status === APPLICATION_STATUS.SCHEDULED || this.isInstalling;
},
installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
@@ -104,30 +135,17 @@ export default {
return (
((this.status !== APPLICATION_STATUS.INSTALLABLE &&
this.status !== APPLICATION_STATUS.ERROR) ||
- this.requestStatus === REQUEST_LOADING ||
- this.requestStatus === REQUEST_SUCCESS) &&
+ this.isInstalling) &&
this.isKnownStatus
);
},
installButtonLabel() {
let label;
- if (
- this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
- this.status === APPLICATION_STATUS.INSTALLABLE ||
- this.status === APPLICATION_STATUS.ERROR ||
- this.isUnknownStatus
- ) {
+ if (this.canInstall) {
label = s__('ClusterIntegration|Install');
- } else if (
- this.status === APPLICATION_STATUS.SCHEDULED ||
- this.status === APPLICATION_STATUS.INSTALLING
- ) {
+ } else if (this.isInstalling) {
label = s__('ClusterIntegration|Installing');
- } else if (
- this.status === APPLICATION_STATUS.INSTALLED ||
- this.status === APPLICATION_STATUS.UPDATED ||
- this.status === APPLICATION_STATUS.UPDATING
- ) {
+ } else if (this.isInstalled) {
label = s__('ClusterIntegration|Installed');
}
@@ -140,13 +158,79 @@ export default {
return s__('ClusterIntegration|Manage');
},
hasError() {
- return this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE;
+ return (
+ !this.isInstalling &&
+ (this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE)
+ );
},
generalErrorDescription() {
return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
title: this.title,
});
},
+ versionLabel() {
+ if (this.upgradeFailed) {
+ return s__('ClusterIntegration|Upgrade failed');
+ } else if (this.isUpgrading) {
+ return s__('ClusterIntegration|Upgrading');
+ }
+
+ return s__('ClusterIntegration|Upgraded');
+ },
+ upgradeRequested() {
+ return this.requestStatus === UPGRADE_REQUESTED;
+ },
+ upgradeSuccessful() {
+ return this.status === APPLICATION_STATUS.UPDATED;
+ },
+ upgradeFailed() {
+ if (this.isUpgrading) {
+ return false;
+ }
+
+ return this.status === APPLICATION_STATUS.UPDATE_ERRORED;
+ },
+ upgradeFailureDescription() {
+ return sprintf(
+ s__(
+ 'ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again.',
+ ),
+ {
+ title: this.title,
+ },
+ );
+ },
+ upgradeSuccessDescription() {
+ return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), {
+ title: this.title,
+ });
+ },
+ upgradeButtonLabel() {
+ let label;
+ if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) {
+ label = s__('ClusterIntegration|Upgrade');
+ } else if (this.isUpgrading) {
+ label = s__('ClusterIntegration|Upgrading');
+ } else if (this.upgradeFailed) {
+ label = s__('ClusterIntegration|Retry upgrade');
+ }
+
+ return label;
+ },
+ isUpgrading() {
+ // Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
+ return (
+ this.status === APPLICATION_STATUS.UPDATING ||
+ (this.upgradeRequested && !this.upgradeSuccessful)
+ );
+ },
+ },
+ watch: {
+ status() {
+ if (this.status === APPLICATION_STATUS.UPDATE_ERRORED) {
+ eventHub.$emit('upgradeFailed', this.id);
+ }
+ },
},
methods: {
installClicked() {
@@ -155,6 +239,15 @@ export default {
params: this.installApplicationRequestParams,
});
},
+ upgradeClicked() {
+ eventHub.$emit('upgradeApplication', {
+ id: this.id,
+ params: this.installApplicationRequestParams,
+ });
+ },
+ dismissUpgradeSuccess() {
+ eventHub.$emit('dismissUpgradeSuccess', this.id);
+ },
},
};
</script>
@@ -208,6 +301,51 @@ export default {
</li>
</ul>
</div>
+
+ <div
+ v-if="(upgradeSuccessful || upgradeFailed) && !upgradeAvailable"
+ class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
+ >
+ {{ versionLabel }}
+
+ <span v-if="upgradeSuccessful"> to</span>
+
+ <gl-link
+ v-if="upgradeSuccessful"
+ :href="chartRepo"
+ target="_blank"
+ class="js-cluster-application-upgrade-version"
+ >
+ chart v{{ version }}
+ </gl-link>
+ </div>
+
+ <div
+ v-if="upgradeFailed && !isUpgrading"
+ class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
+ >
+ {{ upgradeFailureDescription }}
+ </div>
+
+ <div
+ v-if="upgradeRequested && upgradeSuccessful"
+ class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3"
+ >
+ {{ upgradeSuccessDescription }}
+
+ <button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess">
+ &times;
+ </button>
+ </div>
+
+ <loading-button
+ v-if="upgradeAvailable || upgradeFailed || isUpgrading"
+ class="btn btn-primary js-cluster-application-upgrade-button mt-2"
+ :loading="isUpgrading"
+ :disabled="isUpgrading"
+ :label="upgradeButtonLabel"
+ @click="upgradeClicked"
+ />
</div>
<div
:class="{ 'section-25': showManageButton, 'section-15': !showManageButton }"
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 5d19c79570a..0cf187d4189 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -362,6 +362,9 @@ export default {
:status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason"
+ :version="applications.runner.version"
+ :chart-repo="applications.runner.chartRepo"
+ :upgrade-available="applications.runner.upgradeAvailable"
:disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/"
>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index e31afadf186..39022879d91 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -12,16 +12,19 @@ export const APPLICATION_STATUS = {
SCHEDULED: 'scheduled',
INSTALLING: 'installing',
INSTALLED: 'installed',
- UPDATED: 'updated',
UPDATING: 'updating',
+ UPDATED: 'updated',
+ UPDATE_ERRORED: 'update_errored',
ERROR: 'errored',
};
// These are only used client-side
-export const REQUEST_LOADING = 'request-loading';
-export const REQUEST_SUCCESS = 'request-success';
+export const REQUEST_SUBMITTED = 'request-submitted';
export const REQUEST_FAILURE = 'request-failure';
+export const UPGRADE_REQUESTED = 'upgrade-requested';
+export const UPGRADE_REQUEST_FAILURE = 'upgrade-request-failure';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
+export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager';
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 8f74be4e0e6..d309678be27 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,6 +1,6 @@
import { s__ } from '../../locale';
import { parseBoolean } from '../../lib/utils/common_utils';
-import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER } from '../constants';
+import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER, RUNNER } from '../constants';
export default class ClusterStore {
constructor() {
@@ -40,6 +40,9 @@ export default class ClusterStore {
statusReason: null,
requestStatus: null,
requestReason: null,
+ version: null,
+ chartRepo: 'https://gitlab.com/charts/gitlab-runner',
+ upgradeAvailable: null,
},
prometheus: {
title: s__('ClusterIntegration|Prometheus'),
@@ -100,7 +103,13 @@ export default class ClusterStore {
this.state.statusReason = serverState.status_reason;
serverState.applications.forEach(serverAppEntry => {
- const { name: appId, status, status_reason: statusReason } = serverAppEntry;
+ const {
+ name: appId,
+ status,
+ status_reason: statusReason,
+ version,
+ update_available: upgradeAvailable,
+ } = serverAppEntry;
this.state.applications[appId] = {
...(this.state.applications[appId] || {}),
@@ -124,6 +133,9 @@ export default class ClusterStore {
serverAppEntry.hostname || this.state.applications.knative.hostname;
this.state.applications.knative.externalIp =
serverAppEntry.external_ip || this.state.applications.knative.externalIp;
+ } else if (appId === RUNNER) {
+ this.state.applications.runner.version = version;
+ this.state.applications.runner.upgradeAvailable = upgradeAvailable;
}
});
}
diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js
index a7ed175f7a4..009153d0703 100644
--- a/app/assets/javascripts/commons/jquery.js
+++ b/app/assets/javascripts/commons/jquery.js
@@ -7,4 +7,3 @@ import 'vendor/jquery.caret';
import 'vendor/jquery.atwho';
import 'vendor/jquery.scrollTo';
import 'jquery.waitforimages';
-import 'select2/select2';
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index d4c1b07093d..f0ce2579ee7 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -129,6 +129,10 @@ export default {
created() {
this.adjustView();
eventHub.$once('fetchedNotesData', this.setDiscussions);
+ eventHub.$once('fetchDiffData', this.fetchData);
+ },
+ beforeDestroy() {
+ eventHub.$off('fetchDiffData', this.fetchData);
},
methods: {
...mapActions(['startTaskList']),
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 3ef54752436..0bf2dde8b96 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -6,6 +6,7 @@ import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
import SettingsDropdown from './settings_dropdown.vue';
+import DiffStats from './diff_stats.vue';
export default {
components: {
@@ -14,6 +15,7 @@ export default {
GlLink,
GlButton,
SettingsDropdown,
+ DiffStats,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -35,8 +37,15 @@ export default {
},
},
computed: {
- ...mapState('diffs', ['commit', 'showTreeList', 'startVersion', 'latestVersionPath']),
- ...mapGetters('diffs', ['hasCollapsedFile']),
+ ...mapGetters('diffs', ['hasCollapsedFile', 'diffFilesLength']),
+ ...mapState('diffs', [
+ 'commit',
+ 'showTreeList',
+ 'startVersion',
+ 'latestVersionPath',
+ 'addedLines',
+ 'removedLines',
+ ]),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
@@ -104,6 +113,11 @@ export default {
<gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link>
</div>
<div class="inline-parallel-buttons d-none d-md-flex ml-auto">
+ <diff-stats
+ :diff-files-length="diffFilesLength"
+ :added-lines="addedLines"
+ :removed-lines="removedLines"
+ />
<gl-button
v-if="commit || startVersion"
:href="latestVersionPath"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index b58f704bebb..60586d4a607 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -9,6 +9,7 @@ import { GlTooltipDirective } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
import EditButton from './edit_button.vue';
+import DiffStats from './diff_stats.vue';
export default {
components: {
@@ -16,6 +17,7 @@ export default {
EditButton,
Icon,
FileIcon,
+ DiffStats,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -202,6 +204,7 @@ export default {
v-if="!diffFile.submodule && addMergeRequestButtons"
class="file-actions d-none d-sm-block"
>
+ <diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
<template v-if="diffFile.blob && diffFile.blob.readable_text">
<button
:disabled="!diffHasDiscussions(diffFile)"
diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue
new file mode 100644
index 00000000000..2e5855380af
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_stats.vue
@@ -0,0 +1,52 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import { n__ } from '~/locale';
+
+export default {
+ components: { Icon },
+ props: {
+ addedLines: {
+ type: Number,
+ required: true,
+ },
+ removedLines: {
+ type: Number,
+ required: true,
+ },
+ diffFilesLength: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ filesText() {
+ return n__('File', 'Files', this.diffFilesLength);
+ },
+ isCompareVersionsHeader() {
+ return Boolean(this.diffFilesLength);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="diff-stats"
+ :class="{
+ 'is-compare-versions-header d-none d-lg-inline-flex': isCompareVersionsHeader,
+ 'd-inline-flex': !isCompareVersionsHeader,
+ }"
+ >
+ <div v-if="diffFilesLength !== null" class="diff-stats-group">
+ <icon name="doc-code" class="diff-stats-icon text-secondary" />
+ <strong>{{ diffFilesLength }} {{ filesText }}</strong>
+ </div>
+ <div class="diff-stats-group cgreen">
+ <icon name="file-addition" class="diff-stats-icon" /> <strong>{{ addedLines }}</strong>
+ </div>
+ <div class="diff-stats-group cred">
+ <icon name="file-deletion" class="diff-stats-icon" /> <strong>{{ removedLines }}</strong>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 0b3def3d29d..96ae197d8b8 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -13,39 +13,17 @@ export default {
Icon,
FileRow,
},
- data() {
- return {
- search: '',
- };
- },
computed: {
- ...mapState('diffs', ['tree', 'addedLines', 'removedLines', 'renderTreeList']),
- ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']),
+ ...mapState('diffs', ['tree', 'renderTreeList']),
+ ...mapGetters('diffs', ['allBlobs']),
filteredTreeList() {
- const search = this.search.toLowerCase().trim();
-
- if (search === '') return this.renderTreeList ? this.tree : this.allBlobs;
-
- return this.allBlobs.reduce((acc, folder) => {
- const tree = folder.tree.filter(f => f.path.toLowerCase().indexOf(search) >= 0);
-
- if (tree.length) {
- return acc.concat({
- ...folder,
- tree,
- });
- }
-
- return acc;
- }, []);
+ return this.renderTreeList ? this.tree : this.allBlobs;
},
},
methods: {
- ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
- clearSearch() {
- this.search = '';
- },
+ ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']),
},
+ shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '&#8984;' : 'Ctrl'}+P`,
FileRowStats,
};
</script>
@@ -55,21 +33,17 @@ export default {
<div class="append-bottom-8 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<icon name="search" class="position-absolute tree-list-icon" />
- <input
- v-model="search"
- :placeholder="s__('MergeRequest|Filter files')"
- type="search"
- class="form-control"
- />
<button
- v-show="search"
- :aria-label="__('Clear search')"
type="button"
- class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0"
- @click="clearSearch"
+ class="form-control text-left text-secondary"
+ @click="toggleFileFinder(true)"
>
- <icon name="close" />
+ {{ s__('MergeRequest|Search files') }}
</button>
+ <span
+ class="position-absolute text-secondary diff-tree-search-shortcut"
+ v-html="$options.shortcutKeyCharacter"
+ ></span>
</div>
</div>
<div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll">
@@ -90,13 +64,6 @@ export default {
{{ s__('MergeRequest|No files found') }}
</p>
</div>
- <div v-once class="pt-3 pb-3 text-center">
- {{ n__('%d changed file', '%d changed files', diffFilesLength) }}
- <div>
- <span class="cgreen"> {{ n__('%d addition', '%d additions', addedLines) }} </span>
- <span class="cred"> {{ n__('%d deleted', '%d deletions', removedLines) }} </span>
- </div>
- </div>
</div>
</template>
@@ -104,4 +71,15 @@ export default {
.tree-list-blobs .file-row-name {
margin-left: 12px;
}
+
+.diff-tree-search-shortcut {
+ top: 50%;
+ right: 10px;
+ transform: translateY(-50%);
+ pointer-events: none;
+}
+
+.tree-list-icon {
+ pointer-events: none;
+}
</style>
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 094e5cdea9c..63954d9d412 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -1,11 +1,60 @@
import Vue from 'vue';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
+import FindFile from '~/vue_shared/components/file_finder/index.vue';
+import eventHub from '../notes/event_hub';
import diffsApp from './components/app.vue';
import { TREE_LIST_STORAGE_KEY } from './constants';
export default function initDiffsApp(store) {
+ const fileFinderEl = document.getElementById('js-diff-file-finder');
+
+ if (fileFinderEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: fileFinderEl,
+ store,
+ computed: {
+ ...mapState('diffs', ['fileFinderVisible', 'isLoading']),
+ ...mapGetters('diffs', ['flatBlobsList']),
+ },
+ watch: {
+ fileFinderVisible(newVal, oldVal) {
+ if (newVal && !oldVal && !this.flatBlobsList.length) {
+ eventHub.$emit('fetchDiffData');
+ }
+ },
+ },
+ methods: {
+ ...mapActions('diffs', ['toggleFileFinder', 'scrollToFile']),
+ openFile(file) {
+ window.mrTabs.tabShown('diffs');
+ this.scrollToFile(file.path);
+ },
+ },
+ render(createElement) {
+ return createElement(FindFile, {
+ props: {
+ files: this.flatBlobsList,
+ visible: this.fileFinderVisible,
+ loading: this.isLoading,
+ showDiffStats: true,
+ clearSearchOnClose: false,
+ },
+ on: {
+ toggle: this.toggleFileFinder,
+ click: this.openFile,
+ },
+ class: ['diff-file-finder'],
+ style: {
+ display: this.fileFinderVisible ? '' : 'none',
+ },
+ });
+ },
+ });
+ }
+
return new Vue({
el: '#js-diffs-app',
name: 'MergeRequestDiffs',
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 2c5019fb652..7fb66ce433b 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -296,5 +296,9 @@ export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = fals
}
};
+export const toggleFileFinder = ({ commit }, visible) => {
+ commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 86c0c7190f9..0e1ad654a2b 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -74,24 +74,25 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.file_hash === fileHash);
-export const allBlobs = state =>
- Object.values(state.treeEntries)
- .filter(f => f.type === 'blob')
- .reduce((acc, file) => {
- const { parentPath } = file;
-
- if (parentPath && !acc.some(f => f.path === parentPath)) {
- acc.push({
- path: parentPath,
- isHeader: true,
- tree: [],
- });
- }
-
- acc.find(f => f.path === parentPath).tree.push(file);
-
- return acc;
- }, []);
+export const flatBlobsList = state =>
+ Object.values(state.treeEntries).filter(f => f.type === 'blob');
+
+export const allBlobs = (state, getters) =>
+ getters.flatBlobsList.reduce((acc, file) => {
+ const { parentPath } = file;
+
+ if (parentPath && !acc.some(f => f.path === parentPath)) {
+ acc.push({
+ path: parentPath,
+ isHeader: true,
+ tree: [],
+ });
+ }
+
+ acc.find(f => f.path === parentPath).tree.push(file);
+
+ return acc;
+ }, []);
export const diffFilesLength = state => state.diffFiles.length;
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 05b4c552f6e..47f78a5db54 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -11,6 +11,8 @@ const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY);
export default () => ({
isLoading: true,
+ addedLines: null,
+ removedLines: null,
endpoint: '',
basePath: '',
commit: null,
@@ -29,4 +31,5 @@ export default () => ({
highlightedRow: null,
renderTreeList: true,
showWhitespace: true,
+ fileFinderVisible: false,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index e760b4d1079..71ad108ce88 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -22,3 +22,4 @@ export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW';
export const SET_TREE_DATA = 'SET_TREE_DATA';
export const SET_RENDER_TREE_LIST = 'SET_RENDER_TREE_LIST';
export const SET_SHOW_WHITESPACE = 'SET_SHOW_WHITESPACE';
+export const TOGGLE_FILE_FINDER_VISIBLE = 'TOGGLE_FILE_FINDER_VISIBLE';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 4aeb393b29b..7bbafe66199 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -244,4 +244,7 @@ export default {
[types.SET_SHOW_WHITESPACE](state, showWhitespace) {
state.showWhitespace = showWhitespace;
},
+ [types.TOGGLE_FILE_FINDER_VISIBLE](state, visible) {
+ state.fileFinderVisible = visible;
+ },
};
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index dbfcf8cc921..cb1b1173190 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -64,6 +64,7 @@ class DueDateSelect {
this.saveDueDate(true);
}
},
+ firstDay: gon.first_day_of_week,
});
calendar.setDate(parsePikadayDate($dueDateInput.val()));
@@ -183,6 +184,7 @@ export default class DueDateSelectors {
onSelect(dateText) {
$datePicker.val(calendar.toString(dateText));
},
+ firstDay: gon.first_day_of_week,
});
calendar.setDate(parsePikadayDate(datePickerVal));
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index bd402c0eea5..6ece8b92a30 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -22,10 +22,6 @@ export default {
type: Object,
required: true,
},
- canCreateDeployment: {
- type: Boolean,
- required: true,
- },
canReadEnvironment: {
type: Boolean,
required: true,
@@ -51,11 +47,7 @@ export default {
<slot name="emptyState"></slot>
<div v-if="!isLoading && environments.length > 0" class="table-holder">
- <environment-table
- :environments="environments"
- :can-create-deployment="canCreateDeployment"
- :can-read-environment="canReadEnvironment"
- />
+ <environment-table :environments="environments" :can-read-environment="canReadEnvironment" />
<table-pagination
v-if="pagination && pagination.totalPages > 1"
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index f44806d82a6..503c1b38f71 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -47,12 +47,6 @@ export default {
default: () => ({}),
},
- canCreateDeployment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
canReadEnvironment: {
type: Boolean,
required: false,
@@ -151,7 +145,7 @@ export default {
},
actions() {
- if (!this.model || !this.model.last_deployment || !this.canCreateDeployment) {
+ if (!this.model || !this.model.last_deployment) {
return [];
}
@@ -561,7 +555,7 @@ export default {
/>
<rollback-component
- v-if="canRetry && canCreateDeployment"
+ v-if="canRetry"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
/>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 87c1d44dd40..aa2417d3194 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -24,10 +24,6 @@ export default {
type: Boolean,
required: true,
},
- canCreateDeployment: {
- type: Boolean,
- required: true,
- },
canReadEnvironment: {
type: Boolean,
required: true,
@@ -106,7 +102,6 @@ export default {
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
- :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 75bdf87137f..e2c304de00a 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -23,12 +23,6 @@ export default {
required: false,
default: false,
},
-
- canCreateDeployment: {
- type: Boolean,
- required: false,
- default: false,
- },
},
methods: {
folderUrl(model) {
@@ -64,7 +58,6 @@ export default {
is="environment-item"
:key="`environment-item-${i}`"
:model="model"
- :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
@@ -79,7 +72,6 @@ export default {
v-for="(children, index) in model.children"
:key="`env-item-${i}-${index}`"
:model="children"
- :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 982e550e73c..56e7f69cad6 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -18,7 +18,6 @@ export default () =>
endpoint: environmentsData.environmentsDataEndpoint,
folderName: environmentsData.environmentsDataFolderName,
cssContainerClass: environmentsData.cssClass,
- canCreateDeployment: parseBoolean(environmentsData.environmentsDataCanCreateDeployment),
canReadEnvironment: parseBoolean(environmentsData.environmentsDataCanReadEnvironment),
};
},
@@ -28,7 +27,6 @@ export default () =>
endpoint: this.endpoint,
folderName: this.folderName,
cssContainerClass: this.cssContainerClass,
- canCreateDeployment: this.canCreateDeployment,
canReadEnvironment: this.canReadEnvironment,
},
});
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index d6f0b6115a6..80f0e00400b 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -23,10 +23,6 @@ export default {
type: String,
required: true,
},
- canCreateDeployment: {
- type: Boolean,
- required: true,
- },
canReadEnvironment: {
type: Boolean,
required: true,
@@ -55,7 +51,6 @@ export default {
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
- :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
/>
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index d366e7550b7..6af66d0f86e 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -20,7 +20,6 @@ export default () =>
helpPagePath: environmentsData.helpPagePath,
cssContainerClass: environmentsData.cssClass,
canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
- canCreateDeployment: parseBoolean(environmentsData.canCreateDeployment),
canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment),
};
},
@@ -32,7 +31,6 @@ export default () =>
helpPagePath: this.helpPagePath,
cssContainerClass: this.cssContainerClass,
canCreateEnvironment: this.canCreateEnvironment,
- canCreateDeployment: this.canCreateDeployment,
canReadEnvironment: this.canReadEnvironment,
},
});
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 96dc1f07cb9..e81a1525df0 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -143,7 +143,7 @@ export default {
*/
created() {
this.service = new EnvironmentsService(this.endpoint);
- this.requestData = { page: this.page, scope: this.scope };
+ this.requestData = { page: this.page, scope: this.scope, nested: true };
this.poll = new Poll({
resource: this.service,
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 4e07ccba91a..cb4ff6856db 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -7,8 +7,8 @@ export default class EnvironmentsService {
}
fetchEnvironments(options = {}) {
- const { scope, page } = options;
- return axios.get(this.environmentsEndpoint, { params: { scope, page } });
+ const { scope, page, nested } = options;
+ return axios.get(this.environmentsEndpoint, { params: { scope, page, nested } });
}
// eslint-disable-next-line class-methods-use-this
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 5808a2d4afa..ac9a31c202c 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -20,7 +20,8 @@ export default class EnvironmentsStore {
*
* Stores the received environments.
*
- * In the main environments endpoint, each environment has the following schema
+ * In the main environments endpoint (with { nested: true } in params), each folder
+ * has the following schema:
* { name: String, size: Number, latest: Object }
* In the endpoint to retrieve environments from each folder, the environment does
* not have the `latest` key and the data is all in the root level.
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 2049760fe29..bdadbb1bb2a 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -4,93 +4,97 @@ import Api from './api';
import { normalizeHeaders } from './lib/utils/common_utils';
export default function groupsSelect() {
- // Needs to be accessible in rspec
- window.GROUP_SELECT_PER_PAGE = 20;
- $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
- const $select = $(this);
- const allAvailable = $select.data('allAvailable');
- const skipGroups = $select.data('skipGroups') || [];
- const parentGroupID = $select.data('parentId');
- const groupsPath = parentGroupID
- ? Api.subgroupsPath.replace(':id', parentGroupID)
- : Api.groupsPath;
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ // Needs to be accessible in rspec
+ window.GROUP_SELECT_PER_PAGE = 20;
+ $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
+ const $select = $(this);
+ const allAvailable = $select.data('allAvailable');
+ const skipGroups = $select.data('skipGroups') || [];
+ const parentGroupID = $select.data('parentId');
+ const groupsPath = parentGroupID
+ ? Api.subgroupsPath.replace(':id', parentGroupID)
+ : Api.groupsPath;
- $select.select2({
- placeholder: 'Search for a group',
- allowClear: $select.hasClass('allowClear'),
- multiple: $select.hasClass('multiselect'),
- minimumInputLength: 0,
- ajax: {
- url: Api.buildUrl(groupsPath),
- dataType: 'json',
- quietMillis: 250,
- transport(params) {
- axios[params.type.toLowerCase()](params.url, {
- params: params.data,
- })
- .then(res => {
- const results = res.data || [];
- const headers = normalizeHeaders(res.headers);
- const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
- const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
- const more = currentPage < totalPages;
+ $select.select2({
+ placeholder: 'Search for a group',
+ allowClear: $select.hasClass('allowClear'),
+ multiple: $select.hasClass('multiselect'),
+ minimumInputLength: 0,
+ ajax: {
+ url: Api.buildUrl(groupsPath),
+ dataType: 'json',
+ quietMillis: 250,
+ transport(params) {
+ axios[params.type.toLowerCase()](params.url, {
+ params: params.data,
+ })
+ .then(res => {
+ const results = res.data || [];
+ const headers = normalizeHeaders(res.headers);
+ const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
+ const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
+ const more = currentPage < totalPages;
- params.success({
- results,
- pagination: {
- more,
- },
- });
- })
- .catch(params.error);
- },
- data(search, page) {
- return {
- search,
- page,
- per_page: window.GROUP_SELECT_PER_PAGE,
- all_available: allAvailable,
- };
- },
- results(data, page) {
- if (data.length) return { results: [] };
+ params.success({
+ results,
+ pagination: {
+ more,
+ },
+ });
+ })
+ .catch(params.error);
+ },
+ data(search, page) {
+ return {
+ search,
+ page,
+ per_page: window.GROUP_SELECT_PER_PAGE,
+ all_available: allAvailable,
+ };
+ },
+ results(data, page) {
+ if (data.length) return { results: [] };
- const groups = data.length ? data : data.results || [];
- const more = data.pagination ? data.pagination.more : false;
- const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
+ const groups = data.length ? data : data.results || [];
+ const more = data.pagination ? data.pagination.more : false;
+ const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
- return {
- results,
- page,
- more,
- };
- },
- },
- // eslint-disable-next-line consistent-return
- initSelection(element, callback) {
- const id = $(element).val();
- if (id !== '') {
- return Api.group(id, callback);
- }
- },
- formatResult(object) {
- return `<div class='group-result'> <div class='group-name'>${
- object.full_name
- }</div> <div class='group-path'>${object.full_path}</div> </div>`;
- },
- formatSelection(object) {
- return object.full_name;
- },
- dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
- });
+ return {
+ results,
+ page,
+ more,
+ };
+ },
+ },
+ // eslint-disable-next-line consistent-return
+ initSelection(element, callback) {
+ const id = $(element).val();
+ if (id !== '') {
+ return Api.group(id, callback);
+ }
+ },
+ formatResult(object) {
+ return `<div class='group-result'> <div class='group-name'>${
+ object.full_name
+ }</div> <div class='group-path'>${object.full_path}</div> </div>`;
+ },
+ formatSelection(object) {
+ return object.full_name;
+ },
+ dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup(m) {
+ return m;
+ },
+ });
- $select.on('select2-loaded', () => {
- const dropdown = document.querySelector('.select2-infinite .select2-results');
- dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
- });
- });
+ $select.on('select2-loaded', () => {
+ const dropdown = document.querySelector('.select2-infinite .select2-results');
+ dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
+ });
+ });
+ })
+ .catch(() => {});
}
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index caec8779cac..9894ebb0624 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,20 +1,17 @@
<script>
import Vue from 'vue';
-import Mousetrap from 'mousetrap';
import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
+import FindFile from '~/vue_shared/components/file_finder/index.vue';
import NewModal from './new_dropdown/modal.vue';
import IdeSidebar from './ide_side_bar.vue';
import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue';
-import FindFile from './file_finder/index.vue';
import RightPane from './panes/right.vue';
import ErrorMessage from './error_message.vue';
import CommitEditorHeader from './commit_sidebar/editor_header.vue';
-const originalStopCallback = Mousetrap.stopCallback;
-
export default {
components: {
NewModal,
@@ -42,21 +39,18 @@ export default {
'emptyStateSvgPath',
'currentProjectId',
'errorMessage',
+ 'loading',
+ ]),
+ ...mapGetters([
+ 'activeFile',
+ 'hasChanges',
+ 'someUncommittedChanges',
+ 'isCommitModeActive',
+ 'allBlobs',
]),
- ...mapGetters(['activeFile', 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive']),
},
mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e);
-
- Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
- if (e.preventDefault) {
- e.preventDefault();
- }
-
- this.toggleFileFinder(!this.fileFindVisible);
- });
-
- Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
},
methods: {
...mapActions(['toggleFileFinder']),
@@ -70,17 +64,8 @@ export default {
});
return returnValue;
},
- mousetrapStopCallback(e, el, combo) {
- if (
- (combo === 't' && el.classList.contains('dropdown-input-field')) ||
- el.classList.contains('inputarea')
- ) {
- return true;
- } else if (combo === 'command+p' || combo === 'ctrl+p') {
- return false;
- }
-
- return originalStopCallback(e, el, combo);
+ openFile(file) {
+ this.$router.push(`/project${file.url}`);
},
},
};
@@ -90,7 +75,14 @@ export default {
<article class="ide position-relative d-flex flex-column align-items-stretch">
<error-message v-if="errorMessage" :message="errorMessage" />
<div class="ide-view flex-grow d-flex">
- <find-file v-show="fileFindVisible" />
+ <find-file
+ v-show="fileFindVisible"
+ :files="allBlobs"
+ :visible="fileFindVisible"
+ :loading="loading"
+ @toggle="toggleFileFinder"
+ @click="openFile"
+ />
<ide-sidebar />
<div class="multi-file-edit-pane">
<template v-if="activeFile">
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index f1d40586903..ce577ae85b0 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -107,16 +107,23 @@ export default {
class="commit-sha"
>{{ lastCommit.short_id }}</a
>
- by {{ lastCommit.author_name }}
+ by
+ <user-avatar-image
+ css-classes="ide-status-avatar"
+ :size="18"
+ :img-src="latestPipeline && latestPipeline.commit.author_gravatar_url"
+ :img-alt="lastCommit.author_name"
+ :tooltip-text="lastCommit.author_name"
+ />
+ {{ lastCommit.author_name }}
<time
v-tooltip
:datetime="lastCommit.committed_date"
:title="tooltipTitle(lastCommit.committed_date)"
data-placement="top"
data-container="body"
+ >{{ lastCommitFormatedAge }}</time
>
- {{ lastCommitFormatedAge }}
- </time>
</div>
<div v-if="file" class="ide-status-file">{{ file.name }}</div>
<div v-if="file" class="ide-status-file">{{ file.eol }}</div>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 09245ed0296..804ebae4555 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -1,8 +1,3 @@
-// Fuzzy file finder
-export const MAX_FILE_FINDER_RESULTS = 40;
-export const FILE_FINDER_ROW_HEIGHT = 55;
-export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
-
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
// Commit message textarea
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index d97a950a8b2..24c2f71ae2b 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -202,7 +202,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
dispatch(
'setErrorMessage',
{
- text: __('An error accured whilst committing your changes.'),
+ text: __('An error occurred whilst committing your changes.'),
action: () =>
dispatch('commitChanges').then(() =>
dispatch('setErrorMessage', null, { root: true }),
diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
index 612c524ca1c..e0fb58ef195 100644
--- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js
+++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
@@ -11,10 +11,14 @@ class AutoWidthDropdownSelect {
init() {
const { dropdownClass } = this;
- this.$selectElement.select2({
- dropdownCssClass: dropdownClass,
- ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
- });
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ this.$selectElement.select2({
+ dropdownCssClass: dropdownClass,
+ ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
+ });
+ })
+ .catch(() => {});
return this;
}
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index f3d722409b0..48e7ed1318d 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -7,10 +7,14 @@ export default class IssuableContext {
constructor(currentUser) {
this.userSelect = new UsersSelect(currentUser);
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+ })
+ .catch(() => {});
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
return $(this).submit();
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index c81a2230310..9336b71cfd7 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -44,6 +44,7 @@ export default class IssuableForm {
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)),
+ firstDay: gon.first_day_of_week,
});
calendar.setDate(parsePikadayDate($issuableDueDate.val()));
}
@@ -120,35 +121,39 @@ export default class IssuableForm {
}
initTargetBranchDropdown() {
- this.$targetBranchSelect.select2({
- ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
- ajax: {
- url: this.$targetBranchSelect.data('endpoint'),
- dataType: 'JSON',
- quietMillis: 250,
- data(search) {
- return {
- search,
- };
- },
- results(data) {
- return {
- // `data` keys are translated so we can't just access them with a string based key
- results: data[Object.keys(data)[0]].map(name => ({
- id: name,
- text: name,
- })),
- };
- },
- },
- initSelection(el, callback) {
- const val = el.val();
-
- callback({
- id: val,
- text: val,
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ this.$targetBranchSelect.select2({
+ ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
+ ajax: {
+ url: this.$targetBranchSelect.data('endpoint'),
+ dataType: 'JSON',
+ quietMillis: 250,
+ data(search) {
+ return {
+ search,
+ };
+ },
+ results(data) {
+ return {
+ // `data` keys are translated so we can't just access them with a string based key
+ results: data[Object.keys(data)[0]].map(name => ({
+ id: name,
+ text: name,
+ })),
+ };
+ },
+ },
+ initSelection(el, callback) {
+ const val = el.val();
+
+ callback({
+ id: val,
+ text: val,
+ });
+ },
});
- },
- });
+ })
+ .catch(() => {});
}
}
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index cd569eb3045..bd757a76ee7 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -1,5 +1,7 @@
<script>
import Visibility from 'visibilityjs';
+import { __, s__, sprintf } from '~/locale';
+import createFlash from '~/flash';
import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
@@ -10,7 +12,6 @@ import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
-import { __ } from '~/locale';
export default {
components: {
@@ -107,11 +108,6 @@ export default {
type: String,
required: true,
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
projectPath: {
type: String,
required: true,
@@ -130,6 +126,11 @@ export default {
required: false,
default: true,
},
+ lockVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
const store = new Store({
@@ -141,6 +142,7 @@ export default {
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
taskStatus: this.initialTaskStatus,
+ lock_version: this.lockVersion,
});
return {
@@ -161,6 +163,9 @@ export default {
const titleChanged = this.initialTitleText !== this.store.formState.title;
return descriptionChanged || titleChanged;
},
+ defaultErrorMessage() {
+ return sprintf(s__('Error updating %{issuableType}'), { issuableType: this.issuableType });
+ },
},
created() {
this.service = new Service(this.endpoint);
@@ -207,6 +212,17 @@ export default {
}
return undefined;
},
+ updateStoreState() {
+ return this.service
+ .getData()
+ .then(res => res.data)
+ .then(data => {
+ this.store.updateState(data);
+ })
+ .catch(() => {
+ createFlash(this.defaultErrorMessage);
+ });
+ },
openForm() {
if (!this.showForm) {
@@ -214,6 +230,7 @@ export default {
this.store.setFormState({
title: this.state.titleText,
description: this.state.descriptionText,
+ lock_version: this.state.lock_version,
lockedWarningVisible: false,
updateLoading: false,
});
@@ -232,20 +249,24 @@ export default {
if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
-
- return this.service.getData();
})
- .then(res => res.data)
- .then(data => {
- this.store.updateState(data);
+ .then(this.updateStoreState)
+ .then(() => {
eventHub.$emit('close.form');
})
- .catch(error => {
- if (error && error.name === 'SpamError') {
+ .catch((error = {}) => {
+ const { name, response = {} } = error;
+
+ if (name === 'SpamError') {
this.openRecaptcha();
} else {
- eventHub.$emit('close.form');
- window.Flash(`Error updating ${this.issuableType}`);
+ let errMsg = this.defaultErrorMessage;
+
+ if (response.data && response.data.errors) {
+ errMsg += `. ${response.data.errors.join(' ')}`;
+ }
+
+ createFlash(errMsg);
}
});
},
@@ -269,8 +290,9 @@ export default {
visitUrl(data.web_url);
})
.catch(() => {
- eventHub.$emit('close.form');
- window.Flash(`Error deleting ${this.issuableType}`);
+ createFlash(
+ sprintf(s__('Error deleting %{issuableType}'), { issuableType: this.issuableType }),
+ );
});
},
},
@@ -286,7 +308,6 @@ export default {
:issuable-templates="issuableTemplates"
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
- :markdown-version="markdownVersion"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
@@ -314,6 +335,8 @@ export default {
:task-status="state.taskStatus"
:issuable-type="issuableType"
:update-url="updateEndpoint"
+ :lock-version="state.lock_version"
+ @taskListUpdateFailed="updateStoreState"
/>
<edited-component
v-if="hasUpdated"
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 5ca88d75063..58f14bac8c8 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -1,5 +1,7 @@
<script>
import $ from 'jquery';
+import { s__, sprintf } from '~/locale';
+import createFlash from '~/flash';
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
@@ -35,6 +37,11 @@ export default {
required: false,
default: null,
},
+ lockVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
@@ -67,8 +74,10 @@ export default {
new TaskList({
dataType: this.issuableType,
fieldName: 'description',
+ lockVersion: this.lockVersion,
selector: '.detail-page-description',
onSuccess: this.taskListUpdateSuccess.bind(this),
+ onError: this.taskListUpdateError.bind(this),
});
}
},
@@ -82,6 +91,21 @@ export default {
}
},
+ taskListUpdateError() {
+ createFlash(
+ sprintf(
+ s__(
+ 'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.',
+ ),
+ {
+ issueType: this.issuableType,
+ },
+ ),
+ );
+
+ this.$emit('taskListUpdateFailed');
+ },
+
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 90258c0e044..299130e56ae 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -20,11 +20,6 @@ export default {
type: String,
required: true,
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
canAttachFile: {
type: Boolean,
required: false,
@@ -48,7 +43,6 @@ export default {
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
- :markdown-version="markdownVersion"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 45b60bc3392..eade31f1d14 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -39,11 +39,6 @@ export default {
type: String,
required: true,
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
projectPath: {
type: String,
required: true,
@@ -101,7 +96,6 @@ export default {
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
- :markdown-version="markdownVersion"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
/>
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js
index 32044d6da25..3c17e73ccec 100644
--- a/app/assets/javascripts/issue_show/stores/index.js
+++ b/app/assets/javascripts/issue_show/stores/index.js
@@ -1,3 +1,5 @@
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
export default class Store {
constructor(initialState) {
this.state = initialState;
@@ -6,6 +8,7 @@ export default class Store {
description: '',
lockedWarningVisible: false,
updateLoading: false,
+ lock_version: 0,
};
}
@@ -14,14 +17,10 @@ export default class Store {
this.formState.lockedWarningVisible = true;
}
+ Object.assign(this.state, convertObjectPropsToCamelCase(data));
this.state.titleHtml = data.title;
- this.state.titleText = data.title_text;
this.state.descriptionHtml = data.description;
- this.state.descriptionText = data.description_text;
- this.state.taskStatus = data.task_status;
- this.state.updatedAt = data.updated_at;
- this.state.updatedByName = data.updated_by_name;
- this.state.updatedByPath = data.updated_by_path;
+ this.state.lock_version = data.lock_version;
}
stateShouldUpdate(data) {
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index 91332c21b52..c5076d65ff9 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -39,7 +39,9 @@ export default {
<ci-icon :status="pipeline.details.status" class="vertical-align-middle" />
<span class="font-weight-bold">{{ __('Pipeline') }}</span>
- <a :href="pipeline.path" class="js-pipeline-path link-commit">#{{ pipeline.id }}</a>
+ <a :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path"
+ >#{{ pipeline.id }}</a
+ >
<template v-if="hasRef">
{{ __('from') }}
<a :href="pipeline.ref.path" class="link-commit ref-name">{{ pipeline.ref.name }}</a>
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 062501d1d04..f134a54dd53 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -70,7 +70,18 @@ export default class LabelManager {
const $detachedLabel = $label.detach();
this.toggleLabelPriorityBadge($detachedLabel, action);
- $detachedLabel.appendTo($target);
+
+ const $labelEls = $target.find('li.label-list-item');
+
+ /*
+ * If there is a label element in the target, we'd want to
+ * append the new label just right next to it.
+ */
+ if ($labelEls.length) {
+ $labelEls.last().after($detachedLabel);
+ } else {
+ $detachedLabel.appendTo($target);
+ }
if ($from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 3b6a57dad44..0ceff10a02a 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -221,6 +221,22 @@ export const scrollToElement = element => {
};
/**
+ * Returns a function that can only be invoked once between
+ * each browser screen repaint.
+ * @param {Function} fn
+ */
+export const debounceByAnimationFrame = fn => {
+ let requestId;
+
+ return function debounced(...args) {
+ if (requestId) {
+ window.cancelAnimationFrame(requestId);
+ }
+ requestId = window.requestAnimationFrame(() => fn.apply(this, args));
+ };
+};
+
+/**
this will take in the `name` of the param you want to parse in the url
if the name does not exist this function will return `null`
otherwise it will return the value of the param key provided
@@ -614,10 +630,18 @@ export const spriteIcon = (icon, className = '') => {
/**
* This method takes in object with snake_case property names
- * and returns new object with camelCase property names
+ * and returns a new object with camelCase property names
*
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
+ *
+ * This method also supports additional params in `options` object
+ *
+ * @param {Object} obj - Object to be converted.
+ * @param {Object} options - Object containing additional options.
+ * @param {boolean} options.deep - FLag to allow deep object converting
+ * @param {Array[]} dropKeys - List of properties to discard while building new object
+ * @param {Array[]} ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object
*/
export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
if (obj === null) {
@@ -625,12 +649,26 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
}
const initial = Array.isArray(obj) ? [] : {};
+ const { deep = false, dropKeys = [], ignoreKeyNames = [] } = options;
return Object.keys(obj).reduce((acc, prop) => {
const result = acc;
const val = obj[prop];
- if (options.deep && (isObject(val) || Array.isArray(val))) {
+ // Drop properties from new object if
+ // there are any mentioned in options
+ if (dropKeys.indexOf(prop) > -1) {
+ return acc;
+ }
+
+ // Skip converting properties in new object
+ // if there are any mentioned in options
+ if (ignoreKeyNames.indexOf(prop) > -1) {
+ result[prop] = obj[prop];
+ return acc;
+ }
+
+ if (deep && (isObject(val) || Array.isArray(val))) {
result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(val, options);
} else {
result[convertToCamelCase(prop)] = obj[prop];
diff --git a/app/assets/javascripts/lib/utils/file_upload.js b/app/assets/javascripts/lib/utils/file_upload.js
index b41ffb44971..82ee83e4348 100644
--- a/app/assets/javascripts/lib/utils/file_upload.js
+++ b/app/assets/javascripts/lib/utils/file_upload.js
@@ -1,6 +1,9 @@
export default (buttonSelector, fileSelector) => {
const btn = document.querySelector(buttonSelector);
const fileInput = document.querySelector(fileSelector);
+
+ if (!btn || !fileInput) return;
+
const form = btn.closest('form');
btn.addEventListener('click', () => {
diff --git a/app/assets/javascripts/lib/utils/grammar.js b/app/assets/javascripts/lib/utils/grammar.js
new file mode 100644
index 00000000000..18f9e2ed846
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/grammar.js
@@ -0,0 +1,40 @@
+import { sprintf, s__ } from '~/locale';
+
+/**
+ * Combines each given item into a noun series sentence fragment. It does this
+ * in a way that supports i18n by giving context and punctuation to the locale
+ * functions.
+ *
+ * **Examples:**
+ *
+ * - `["A", "B"] => "A and B"`
+ * - `["A", "B", "C"] => "A, B, and C"`
+ *
+ * **Why only nouns?**
+ *
+ * Some languages need a bit more context to translate other series.
+ *
+ * @param {String[]} items
+ */
+export const toNounSeriesText = items => {
+ if (items.length === 0) {
+ return '';
+ } else if (items.length === 1) {
+ return items[0];
+ } else if (items.length === 2) {
+ return sprintf(s__('nounSeries|%{firstItem} and %{lastItem}'), {
+ firstItem: items[0],
+ lastItem: items[1],
+ });
+ }
+
+ return items.reduce((item, nextItem, idx) =>
+ idx === items.length - 1
+ ? sprintf(s__('nounSeries|%{item}, and %{lastItem}'), { item, lastItem: nextItem })
+ : sprintf(s__('nounSeries|%{item}, %{nextItem}'), { item, nextItem }),
+ );
+};
+
+export default {
+ toNounSeriesText,
+};
diff --git a/app/assets/javascripts/lib/utils/icon_utils.js b/app/assets/javascripts/lib/utils/icon_utils.js
new file mode 100644
index 00000000000..7b8dd9bbef7
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/icon_utils.js
@@ -0,0 +1,18 @@
+/* eslint-disable import/prefer-default-export */
+
+import axios from '~/lib/utils/axios_utils';
+
+/**
+ * Retrieve SVG icon path content from gitlab/svg sprite icons
+ * @param {String} name
+ */
+export const getSvgIconPathContent = name =>
+ axios
+ .get(gon.sprite_icons)
+ .then(({ data: svgs }) =>
+ new DOMParser()
+ .parseFromString(svgs, 'text/xml')
+ .querySelector(`#${name} path`)
+ .getAttribute('d'),
+ )
+ .catch(() => null);
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 4ba3543f9b2..63db4938cd7 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -100,27 +100,30 @@ function deferredInitialisation() {
});
// Initialize select2 selects
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
-
- // Close select2 on escape
- $('.js-select2').on('select2-close', () => {
- setTimeout(() => {
- $('.select2-container-active').removeClass('select2-container-active');
- $(':focus').blur();
- }, 1);
- });
+ if ($('select.select2').length) {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+
+ // Close select2 on escape
+ $('.js-select2').on('select2-close', () => {
+ setTimeout(() => {
+ $('.select2-container-active').removeClass('select2-container-active');
+ $(':focus').blur();
+ }, 1);
+ });
+ })
+ .catch(() => {});
+ }
// Initialize tooltips
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
trigger: 'hover',
boundary: 'viewport',
- placement(tip, el) {
- return $(el).data('placement') || 'bottom';
- },
});
// Initialize popovers
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index 0beedcacf33..0dabb28ea66 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -33,6 +33,7 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d
toggleClearInput.call($input);
},
+ firstDay: gon.first_day_of_week,
});
calendar.setDate(parsePikadayDate($input.val()));
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 0deae478deb..3b42a154af8 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -2,6 +2,7 @@
import $ from 'jquery';
import { __ } from '~/locale';
+import createFlash from '~/flash';
import TaskList from './task_list';
import MergeRequestTabs from './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
@@ -35,10 +36,18 @@ function MergeRequest(opts) {
dataType: 'merge_request',
fieldName: 'description',
selector: '.detail-page-description',
+ lockVersion: this.$el.data('lockVersion'),
onSuccess: result => {
document.querySelector('#task_status').innerText = result.task_status;
document.querySelector('#task_status_short').innerText = result.task_status_short;
},
+ onError: () => {
+ createFlash(
+ __(
+ 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.',
+ ),
+ );
+ },
});
}
}
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index e2cffe0b4b4..14c02db7bcc 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -1,10 +1,16 @@
<script>
-import { GlAreaChart } from '@gitlab/ui';
+import { GlAreaChart } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
+import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
+import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import Icon from '~/vue_shared/components/icon.vue';
+
+let debouncedResize;
export default {
components: {
GlAreaChart,
+ Icon,
},
inheritAttrs: false,
props: {
@@ -26,22 +32,38 @@ export default {
);
},
},
+ containerWidth: {
+ type: Number,
+ required: true,
+ },
+ deploymentData: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
alertData: {
type: Object,
required: false,
default: () => ({}),
},
},
+ data() {
+ return {
+ tooltip: {
+ title: '',
+ content: '',
+ isDeployment: false,
+ sha: '',
+ },
+ width: 0,
+ height: 0,
+ scatterSymbol: undefined,
+ };
+ },
computed: {
chartData() {
return this.graphData.queries.reduce((accumulator, query) => {
- const xLabel = `${query.unit}`;
- accumulator[xLabel] = {};
- query.result.forEach(res =>
- res.values.forEach(v => {
- accumulator[xLabel][v.time.toISOString()] = v.value;
- }),
- );
+ accumulator[query.unit] = query.result.reduce((acc, res) => acc.concat(res.values), []);
return accumulator;
}, {});
},
@@ -51,14 +73,17 @@ export default {
name: 'Time',
type: 'time',
axisLabel: {
- formatter: date => dateFormat(date, 'h:MMtt'),
+ formatter: date => dateFormat(date, 'h:MM TT'),
+ },
+ axisPointer: {
+ snap: true,
},
nameTextStyle: {
padding: [18, 0, 0, 0],
},
},
yAxis: {
- name: this.graphData.y_label,
+ name: this.yAxisLabel,
axisLabel: {
formatter: value => value.toFixed(3),
},
@@ -69,33 +94,129 @@ export default {
legend: {
formatter: this.xAxisLabel,
},
+ series: this.scatterSeries,
+ };
+ },
+ earliestDatapoint() {
+ return Object.values(this.chartData).reduce((acc, data) => {
+ const [[timestamp]] = data.sort(([a], [b]) => {
+ if (a < b) {
+ return -1;
+ }
+ return a > b ? 1 : 0;
+ });
+
+ return timestamp < acc || acc === null ? timestamp : acc;
+ }, null);
+ },
+ recentDeployments() {
+ return this.deploymentData.reduce((acc, deployment) => {
+ if (deployment.created_at >= this.earliestDatapoint) {
+ acc.push({
+ id: deployment.id,
+ createdAt: deployment.created_at,
+ sha: deployment.sha,
+ commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
+ tag: deployment.tag,
+ tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null,
+ ref: deployment.ref.name,
+ showDeploymentFlag: false,
+ });
+ }
+
+ return acc;
+ }, []);
+ },
+ scatterSeries() {
+ return {
+ type: 'scatter',
+ data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]),
+ symbol: this.scatterSymbol,
+ symbolSize: 14,
};
},
xAxisLabel() {
return this.graphData.queries.map(query => query.label).join(', ');
},
+ yAxisLabel() {
+ const [query] = this.graphData.queries;
+ return `${this.graphData.y_label} (${query.unit})`;
+ },
+ },
+ watch: {
+ containerWidth: 'onResize',
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', debouncedResize);
+ },
+ created() {
+ debouncedResize = debounceByAnimationFrame(this.onResize);
+ window.addEventListener('resize', debouncedResize);
+ this.getScatterSymbol();
},
methods: {
formatTooltipText(params) {
- const [date, value] = params;
- return [dateFormat(date, 'dd mmm yyyy, h:MMtt'), value.toFixed(3)];
+ const [seriesData] = params.seriesData;
+ this.tooltip.isDeployment = seriesData.componentSubType === 'scatter';
+ this.tooltip.title = dateFormat(params.value, 'dd mmm yyyy, h:MMTT');
+ if (this.tooltip.isDeployment) {
+ const [deploy] = this.recentDeployments.filter(
+ deployment => deployment.createdAt === seriesData.value[0],
+ );
+ this.tooltip.sha = deploy.sha.substring(0, 8);
+ } else {
+ this.tooltip.content = `${this.yAxisLabel} ${seriesData.value[1].toFixed(3)}`;
+ }
+ },
+ getScatterSymbol() {
+ getSvgIconPathContent('rocket')
+ .then(path => {
+ if (path) {
+ this.scatterSymbol = `path://${path}`;
+ }
+ })
+ .catch(() => {});
+ },
+ onResize() {
+ const { width, height } = this.$refs.areaChart.$el.getBoundingClientRect();
+ this.width = width;
+ this.height = height;
},
},
};
</script>
<template>
- <div class="prometheus-graph">
+ <div class="prometheus-graph col-12 col-lg-6">
<div class="prometheus-graph-header">
- <h5 class="prometheus-graph-title">{{ graphData.title }}</h5>
- <div class="prometheus-graph-widgets"><slot></slot></div>
+ <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
+ <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
</div>
<gl-area-chart
+ ref="areaChart"
v-bind="$attrs"
:data="chartData"
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
:thresholds="alertData"
- />
+ :width="width"
+ :height="height"
+ >
+ <template slot="tooltipTitle">
+ <div v-if="tooltip.isDeployment">
+ {{ __('Deployed') }}
+ </div>
+ {{ tooltip.title }}
+ </template>
+ <template slot="tooltipContent">
+ <div v-if="tooltip.isDeployment" class="d-flex align-items-center">
+ <icon name="commit" class="mr-2" />
+ {{ tooltip.sha }}
+ </div>
+ <template v-else>
+ {{ tooltip.content }}
+ </template>
+ </template>
+ </gl-area-chart>
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index cea5c1a56ca..895a57785bc 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,20 +1,19 @@
<script>
-import _ from 'underscore';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import MonitorAreaChart from './charts/area.vue';
import GraphGroup from './graph_group.vue';
-import Graph from './graph.vue';
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
-import eventHub from '../event_hub';
+
+const sidebarAnimationDuration = 150;
+let sidebarMutationObserver;
export default {
components: {
MonitorAreaChart,
- Graph,
GraphGroup,
EmptyState,
Icon,
@@ -25,21 +24,11 @@ export default {
required: false,
default: true,
},
- showLegend: {
- type: Boolean,
- required: false,
- default: true,
- },
showPanels: {
type: Boolean,
required: false,
default: true,
},
- forceSmallGraph: {
- type: Boolean,
- required: false,
- default: false,
- },
documentationPath: {
type: String,
required: true,
@@ -99,48 +88,32 @@ export default {
store: new MonitoringStore(),
state: 'gettingStarted',
showEmptyState: true,
- hoverData: {},
elWidth: 0,
};
},
- computed: {
- graphComponent() {
- return gon.features && gon.features.areaChart ? MonitorAreaChart : Graph;
- },
- forceRedraw() {
- return this.elWidth;
- },
- },
created() {
this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
environmentsEndpoint: this.environmentsEndpoint,
});
- this.mutationObserverConfig = {
- attributes: true,
- childList: false,
- subtree: false,
- };
- eventHub.$on('hoverChanged', this.hoverChanged);
},
beforeDestroy() {
- eventHub.$off('hoverChanged', this.hoverChanged);
- window.removeEventListener('resize', this.resizeThrottled, false);
- this.sidebarMutationObserver.disconnect();
+ if (sidebarMutationObserver) {
+ sidebarMutationObserver.disconnect();
+ }
},
mounted() {
- this.resizeThrottled = _.debounce(this.resize, 100);
if (!this.hasMetrics) {
this.state = 'gettingStarted';
} else {
this.getGraphsData();
- window.addEventListener('resize', this.resizeThrottled, false);
-
- const sidebarEl = document.querySelector('.nav-sidebar');
- // The sidebar listener
- this.sidebarMutationObserver = new MutationObserver(this.resizeThrottled);
- this.sidebarMutationObserver.observe(sidebarEl, this.mutationObserverConfig);
+ sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
+ sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
+ attributes: true,
+ childList: false,
+ subtree: false,
+ });
}
},
methods: {
@@ -168,42 +141,40 @@ export default {
this.showEmptyState = false;
})
- .then(this.resize)
.catch(() => {
this.state = 'unableToConnect';
});
},
- resize() {
- this.elWidth = this.$el.clientWidth;
- },
- hoverChanged(data) {
- this.hoverData = data;
+ onSidebarMutation() {
+ setTimeout(() => {
+ this.elWidth = this.$el.clientWidth;
+ }, sidebarAnimationDuration);
},
},
};
</script>
<template>
- <div v-if="!showEmptyState" :key="forceRedraw" class="prometheus-graphs prepend-top-default">
+ <div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default">
<div class="environments d-flex align-items-center">
{{ s__('Metrics|Environment') }}
<div class="dropdown prepend-left-10">
<button class="dropdown-menu-toggle" data-toggle="dropdown" type="button">
- <span> {{ currentEnvironmentName }} </span> <icon name="chevron-down" />
+ <span>{{ currentEnvironmentName }}</span>
+ <icon name="chevron-down" />
</button>
<div
v-if="store.environmentsData.length > 0"
class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"
>
<ul>
- <li v-for="environment in store.environmentsData" :key="environment.latest.id">
+ <li v-for="environment in store.environmentsData" :key="environment.id">
<a
- :href="environment.latest.metrics_path"
- :class="{ 'is-active': environment.latest.name == currentEnvironmentName }"
+ :href="environment.metrics_path"
+ :class="{ 'is-active': environment.name == currentEnvironmentName }"
class="dropdown-item"
+ >{{ environment.name }}</a
>
- {{ environment.latest.name }}
- </a>
</li>
</ul>
</div>
@@ -215,23 +186,15 @@ export default {
:name="groupData.group"
:show-panels="showPanels"
>
- <component
- :is="graphComponent"
+ <monitor-area-chart
v-for="(graphData, graphIndex) in groupData.metrics"
:key="graphIndex"
:graph-data="graphData"
- :hover-data="hoverData"
:deployment-data="store.deploymentData"
- :project-path="projectPath"
- :tags-path="tagsPath"
- :show-legend="showLegend"
- :small-graph="forceSmallGraph"
:alert-data="getGraphAlerts(graphData.id)"
+ :container-width="elWidth"
group-id="monitor-area-chart"
- >
- <!-- EE content -->
- {{ null }}
- </component>
+ />
</graph-group>
</div>
<empty-state
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
deleted file mode 100644
index 309b73f5a4d..00000000000
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ /dev/null
@@ -1,329 +0,0 @@
-<script>
-import { scaleLinear, scaleTime } from 'd3-scale';
-import { axisLeft, axisBottom } from 'd3-axis';
-import _ from 'underscore';
-import { max, extent } from 'd3-array';
-import { select } from 'd3-selection';
-import GraphAxis from './graph/axis.vue';
-import GraphLegend from './graph/legend.vue';
-import GraphFlag from './graph/flag.vue';
-import GraphDeployment from './graph/deployment.vue';
-import GraphPath from './graph/path.vue';
-import MonitoringMixin from '../mixins/monitoring_mixins';
-import eventHub from '../event_hub';
-import measurements from '../utils/measurements';
-import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
-import createTimeSeries from '../utils/multiple_time_series';
-import bp from '../../breakpoints';
-
-const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
-
-export default {
- components: {
- GraphAxis,
- GraphFlag,
- GraphDeployment,
- GraphPath,
- GraphLegend,
- },
- mixins: [MonitoringMixin],
- props: {
- graphData: {
- type: Object,
- required: true,
- },
- deploymentData: {
- type: Array,
- required: true,
- },
- hoverData: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- projectPath: {
- type: String,
- required: true,
- },
- tagsPath: {
- type: String,
- required: true,
- },
- showLegend: {
- type: Boolean,
- required: false,
- default: true,
- },
- smallGraph: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- baseGraphHeight: 450,
- baseGraphWidth: 600,
- graphHeight: 450,
- graphWidth: 600,
- graphHeightOffset: 120,
- margin: {},
- unitOfDisplay: '',
- yAxisLabel: '',
- legendTitle: '',
- reducedDeploymentData: [],
- measurements: measurements.large,
- currentData: {
- time: new Date(),
- value: 0,
- },
- currentXCoordinate: 0,
- currentCoordinates: {},
- showFlag: false,
- showFlagContent: false,
- timeSeries: [],
- graphDrawData: {},
- realPixelRatio: 1,
- seriesUnderMouse: [],
- };
- },
- computed: {
- outerViewBox() {
- return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
- },
- innerViewBox() {
- return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
- },
- axisTransform() {
- return `translate(70, ${this.graphHeight - 100})`;
- },
- paddingBottomRootSvg() {
- return {
- paddingBottom: `${Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth || 0}%`,
- };
- },
- deploymentFlagData() {
- return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
- },
- shouldRenderData() {
- return this.graphData.queries.filter(s => s.result.length > 0).length > 0;
- },
- },
- watch: {
- hoverData() {
- this.positionFlag();
- },
- },
- mounted() {
- this.draw();
- },
- methods: {
- showDot(path) {
- return this.showFlagContent && this.seriesUnderMouse.includes(path);
- },
- draw() {
- const breakpointSize = bp.getBreakpointSize();
- const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;
-
- this.margin = measurements.large.margin;
-
- if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
- this.graphHeight = 300;
- this.margin = measurements.small.margin;
- this.measurements = measurements.small;
- }
-
- this.yAxisLabel = this.graphData.y_label || 'Values';
- this.graphWidth = svgWidth - this.margin.left - this.margin.right;
- this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
- this.baseGraphHeight = this.graphHeight - 50;
- this.baseGraphWidth = this.graphWidth;
-
- // pixel offsets inside the svg and outside are not 1:1
- this.realPixelRatio = svgWidth / this.baseGraphWidth;
-
- // set the legends on the axes
- const [query] = this.graphData.queries;
- this.legendTitle = query ? query.label : 'Average';
- this.unitOfDisplay = query ? query.unit : '';
-
- if (this.shouldRenderData) {
- this.renderAxesPaths();
- this.formatDeployments();
- }
- },
- handleMouseOverGraph(e) {
- let point = this.$refs.graphData.createSVGPoint();
- point.x = e.clientX;
- point.y = e.clientY;
- point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
- point.x += 7;
-
- this.seriesUnderMouse = this.timeSeries.filter(series => {
- const mouseX = series.timeSeriesScaleX.invert(point.x);
- let minDistance = Infinity;
-
- const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => {
- const distance = Math.abs(Number(new Date(x)) - Number(mouseX));
- if (distance < minDistance) {
- minDistance = distance;
- return x;
- }
- return closest;
- });
-
- return series.values.find(v => v.time.toString() === closestTickMark);
- });
-
- const firstTimeSeries = this.seriesUnderMouse[0];
- const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
- const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
- const d0 = firstTimeSeries.values[overlayIndex - 1];
- const d1 = firstTimeSeries.values[overlayIndex];
- if (d0 === undefined || d1 === undefined) return;
- const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
- const hoveredDataIndex = evalTime ? overlayIndex : overlayIndex - 1;
- const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
- const currentDeployXPos = this.mouseOverDeployInfo(point.x);
-
- eventHub.$emit('hoverChanged', {
- hoveredDate,
- currentDeployXPos,
- });
- },
- renderAxesPaths() {
- ({ timeSeries: this.timeSeries, graphDrawData: this.graphDrawData } = createTimeSeries(
- this.graphData.queries,
- this.graphWidth,
- this.graphHeight,
- this.graphHeightOffset,
- ));
-
- if (_.findWhere(this.timeSeries, { renderCanary: true })) {
- this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
- }
-
- const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
- const axisYScale = d3.scaleLinear().range([this.graphHeight - this.graphHeightOffset, 0]);
-
- const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
- axisXScale.domain(d3.extent(allValues, d => d.time));
- axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
-
- this.allXAxisValues = this.timeSeries.reduce((obj, series) => {
- const seriesKeys = {};
- series.values.forEach(v => {
- seriesKeys[v.time] = true;
- });
- return {
- ...obj,
- ...seriesKeys,
- };
- }, {});
-
- const xAxis = d3
- .axisBottom()
- .scale(axisXScale)
- .ticks(this.graphWidth / 120)
- .tickFormat(timeScaleFormat);
-
- const yAxis = d3
- .axisLeft()
- .scale(axisYScale)
- .ticks(measurements.yTicks);
-
- d3.select(this.$refs.baseSvg)
- .select('.x-axis')
- .call(xAxis);
-
- const width = this.graphWidth;
- d3.select(this.$refs.baseSvg)
- .select('.y-axis')
- .call(yAxis)
- .selectAll('.tick')
- .each(function createTickLines(d, i) {
- if (i > 0) {
- d3.select(this)
- .select('line')
- .attr('x2', width)
- .attr('class', 'axis-tick');
- } // Avoid adding the class to the first tick, to prevent coloring
- }); // This will select all of the ticks once they're rendered
- },
- },
-};
-</script>
-
-<template>
- <div
- class="prometheus-graph"
- @mouseover="showFlagContent = true"
- @mouseleave="showFlagContent = false"
- >
- <div class="prometheus-graph-header">
- <h5 class="prometheus-graph-title">{{ graphData.title }}</h5>
- <div class="prometheus-graph-widgets"><slot></slot></div>
- </div>
- <div :style="paddingBottomRootSvg" class="prometheus-svg-container">
- <svg ref="baseSvg" :viewBox="outerViewBox">
- <g :transform="axisTransform" class="x-axis" />
- <g class="y-axis" transform="translate(70, 20)" />
- <graph-axis
- :graph-width="graphWidth"
- :graph-height="graphHeight"
- :margin="margin"
- :measurements="measurements"
- :y-axis-label="yAxisLabel"
- :unit-of-display="unitOfDisplay"
- />
- <svg v-if="shouldRenderData" ref="graphData" :viewBox="innerViewBox" class="graph-data">
- <slot name="additionalSvgContent" :graphDrawData="graphDrawData" />
- <graph-path
- v-for="(path, index) in timeSeries"
- :key="index"
- :generated-line-path="path.linePath"
- :generated-area-path="path.areaPath"
- :line-style="path.lineStyle"
- :line-color="path.lineColor"
- :area-color="path.areaColor"
- :current-coordinates="currentCoordinates[path.metricTag]"
- :show-dot="showDot(path)"
- />
- <graph-deployment
- :deployment-data="reducedDeploymentData"
- :graph-height="graphHeight"
- :graph-height-offset="graphHeightOffset"
- />
- <rect
- ref="graphOverlay"
- :width="graphWidth - 70"
- :height="graphHeight - 100"
- class="prometheus-graph-overlay"
- transform="translate(-5, 20)"
- @mousemove="handleMouseOverGraph($event)"
- />
- </svg>
- <svg v-else :viewBox="innerViewBox" class="js-no-data-to-display">
- <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle">
- {{ s__('Metrics|No data to display') }}
- </text>
- </svg>
- </svg>
- <graph-flag
- v-if="shouldRenderData"
- :real-pixel-ratio="realPixelRatio"
- :current-x-coordinate="currentXCoordinate"
- :current-data="currentData"
- :graph-height="graphHeight"
- :graph-height-offset="graphHeightOffset"
- :show-flag-content="showFlagContent"
- :time-series="seriesUnderMouse"
- :unit-of-display="unitOfDisplay"
- :legend-title="legendTitle"
- :deployment-flag-data="deploymentFlagData"
- :current-coordinates="currentCoordinates"
- />
- </div>
- <graph-legend v-if="showLegend" :legend-title="legendTitle" :time-series="timeSeries" />
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue
deleted file mode 100644
index 8f046857a20..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/axis.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-<script>
-import { convertToSentenceCase } from '~/lib/utils/text_utility';
-import { s__ } from '~/locale';
-
-export default {
- props: {
- graphWidth: {
- type: Number,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- margin: {
- type: Object,
- required: true,
- },
- measurements: {
- type: Object,
- required: true,
- },
- yAxisLabel: {
- type: String,
- required: true,
- },
- unitOfDisplay: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- yLabelWidth: 0,
- yLabelHeight: 0,
- };
- },
- computed: {
- textTransform() {
- const yCoordinate =
- (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0;
-
- return `translate(15, ${yCoordinate}) rotate(-90)`;
- },
-
- rectTransform() {
- const yCoordinate =
- (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 +
- this.yLabelWidth / 2 || 0;
-
- return `translate(0, ${yCoordinate}) rotate(-90)`;
- },
-
- xPosition() {
- return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0;
- },
-
- yPosition() {
- return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0;
- },
-
- yAxisLabelSentenceCase() {
- return `${convertToSentenceCase(this.yAxisLabel)} (${this.unitOfDisplay})`;
- },
-
- timeString() {
- return s__('PrometheusDashboard|Time');
- },
- },
- mounted() {
- this.$nextTick(() => {
- const bbox = this.$refs.ylabel.getBBox();
- this.yLabelWidth = bbox.width + 10; // Added some padding
- this.yLabelHeight = bbox.height + 5;
- });
- },
-};
-</script>
-<template>
- <g class="axis-label-container">
- <line
- :y1="yPosition"
- :x2="graphWidth + 20"
- :y2="yPosition"
- class="label-x-axis-line"
- stroke="#000000"
- stroke-width="1"
- x1="10"
- />
- <line
- :x2="10"
- :y2="yPosition"
- class="label-y-axis-line"
- stroke="#000000"
- stroke-width="1"
- x1="10"
- y1="0"
- />
- <rect
- :transform="rectTransform"
- :width="yLabelWidth"
- :height="yLabelHeight"
- class="rect-axis-text"
- />
- <text
- ref="ylabel"
- :transform="textTransform"
- class="label-axis-text y-label-text"
- text-anchor="middle"
- >
- {{ yAxisLabelSentenceCase }}
- </text>
- <rect :x="xPosition + 60" :y="graphHeight - 80" class="rect-axis-text" width="35" height="50" />
- <text :x="xPosition + 60" :y="yPosition" class="label-axis-text x-label-text" dy=".35em">
- {{ timeString }}
- </text>
- </g>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
deleted file mode 100644
index bee9784692c..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/deployment.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<script>
-export default {
- props: {
- deploymentData: {
- type: Array,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- graphHeightOffset: {
- type: Number,
- required: true,
- },
- },
- computed: {
- calculatedHeight() {
- return this.graphHeight - this.graphHeightOffset;
- },
- },
- methods: {
- transformDeploymentGroup(deployment) {
- return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
- },
- },
-};
-</script>
-<template>
- <g class="deploy-info">
- <g
- v-for="(deployment, index) in deploymentData"
- :key="index"
- :transform="transformDeploymentGroup(deployment)"
- >
- <rect :height="calculatedHeight" x="0" y="0" width="3" fill="url(#shadow-gradient)" />
- <line :y2="calculatedHeight" class="deployment-line" x1="0" y1="0" x2="0" stroke="#000" />
- </g>
- <svg height="0" width="0">
- <defs>
- <linearGradient id="shadow-gradient">
- <stop offset="0%" stop-color="#000" stop-opacity="0.4" />
- <stop offset="100%" stop-color="#000" stop-opacity="0" />
- </linearGradient>
- </defs>
- </svg>
- </g>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
deleted file mode 100644
index 9d6d1caef80..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ /dev/null
@@ -1,151 +0,0 @@
-<script>
-import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
-import { formatRelevantDigits } from '../../../lib/utils/number_utils';
-import Icon from '../../../vue_shared/components/icon.vue';
-import TrackLine from './track_line.vue';
-
-export default {
- components: {
- Icon,
- TrackLine,
- },
- props: {
- currentXCoordinate: {
- type: Number,
- required: true,
- },
- currentData: {
- type: Object,
- required: true,
- },
- deploymentFlagData: {
- type: Object,
- required: false,
- default: null,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- graphHeightOffset: {
- type: Number,
- required: true,
- },
- realPixelRatio: {
- type: Number,
- required: true,
- },
- showFlagContent: {
- type: Boolean,
- required: true,
- },
- timeSeries: {
- type: Array,
- required: true,
- },
- unitOfDisplay: {
- type: String,
- required: true,
- },
- legendTitle: {
- type: String,
- required: true,
- },
- currentCoordinates: {
- type: Object,
- required: true,
- },
- },
- computed: {
- formatTime() {
- return this.deploymentFlagData
- ? timeFormat(this.deploymentFlagData.time)
- : timeFormat(this.currentData.time);
- },
- formatDate() {
- return this.deploymentFlagData
- ? dateFormat(this.deploymentFlagData.time)
- : dateFormat(this.currentData.time);
- },
- cursorStyle() {
- const xCoordinate = this.deploymentFlagData
- ? this.deploymentFlagData.xPos
- : this.currentXCoordinate;
-
- const offsetTop = 20 * this.realPixelRatio;
- const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
- const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
-
- return {
- top: `${offsetTop}px`,
- left: `${offsetLeft}px`,
- height: `${height}px`,
- };
- },
- flagOrientation() {
- if (this.currentXCoordinate * this.realPixelRatio > 120) {
- return 'left';
- }
- return 'right';
- },
- },
- methods: {
- seriesMetricValue(seriesIndex, series) {
- const indexFromCoordinates = this.currentCoordinates[series.metricTag]
- ? this.currentCoordinates[series.metricTag].currentDataIndex
- : 0;
- const index = this.deploymentFlagData
- ? this.deploymentFlagData.seriesIndex
- : indexFromCoordinates;
- const value = series.values[index] && series.values[index].value;
- if (Number.isNaN(value)) {
- return '-';
- }
- return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
- },
- seriesMetricLabel(index, series) {
- if (this.timeSeries.length < 2) {
- return this.legendTitle;
- }
- if (series.metricTag) {
- return series.metricTag;
- }
- return `series ${index + 1}`;
- },
- },
-};
-</script>
-
-<template>
- <div :style="cursorStyle" class="prometheus-graph-cursor">
- <div v-if="showFlagContent" :class="flagOrientation" class="prometheus-graph-flag popover">
- <div class="arrow-shadow"></div>
- <div class="arrow"></div>
- <div class="popover-title">
- <h5 v-if="deploymentFlagData">Deployed</h5>
- {{ formatDate }} <strong>{{ formatTime }}</strong>
- </div>
- <div v-if="deploymentFlagData" class="popover-content deploy-meta-content">
- <div>
- <icon :size="12" name="commit" />
- <a :href="deploymentFlagData.commitUrl"> {{ deploymentFlagData.sha.slice(0, 8) }} </a>
- </div>
- <div v-if="deploymentFlagData.tag">
- <icon :size="12" name="label" />
- <a :href="deploymentFlagData.tagUrl"> {{ deploymentFlagData.ref }} </a>
- </div>
- </div>
- <div class="popover-content">
- <table class="prometheus-table">
- <tr v-for="(series, index) in timeSeries" :key="index">
- <track-line :track="series" />
- <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
- <td>
- <strong>{{ seriesMetricValue(index, series) }}</strong>
- </td>
- </tr>
- </table>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
deleted file mode 100644
index b5211c306a3..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<script>
-import TrackLine from './track_line.vue';
-import TrackInfo from './track_info.vue';
-
-export default {
- components: {
- TrackLine,
- TrackInfo,
- },
- props: {
- legendTitle: {
- type: String,
- required: true,
- },
- timeSeries: {
- type: Array,
- required: true,
- },
- },
- methods: {
- isStable(track) {
- return {
- 'prometheus-table-row-highlight': track.trackName !== 'Canary' && track.renderCanary,
- };
- },
- },
-};
-</script>
-<template>
- <div class="prometheus-graph-legends prepend-left-10">
- <table class="prometheus-table">
- <tr
- v-for="(series, index) in timeSeries"
- v-if="series.shouldRenderLegend"
- :key="index"
- :class="isStable(series)"
- >
- <td>
- <strong v-if="series.renderCanary">{{ series.trackName }}</strong>
- </td>
- <track-line :track="series" />
- <td v-if="timeSeries.length > 1" class="legend-metric-title">
- <track-info v-if="series.metricTag" :track="series" />
- <track-info v-else :track="series">
- <strong>{{ legendTitle }}</strong> series {{ index + 1 }}
- </track-info>
- </td>
- <td v-else>
- <track-info :track="series">
- <strong>{{ legendTitle }}</strong>
- </track-info>
- </td>
- <template v-for="(track, trackIndex) in series.tracksLegend">
- <track-line :key="`track-line-${trackIndex}`" :track="track" />
- <td :key="`track-info-${trackIndex}`">
- <track-info :track="track" class="legend-metric-title" />
- </td>
- </template>
- </tr>
- </table>
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
deleted file mode 100644
index f2c237ec391..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-export default {
- props: {
- generatedLinePath: {
- type: String,
- required: true,
- },
- generatedAreaPath: {
- type: String,
- required: true,
- },
- lineStyle: {
- type: String,
- required: false,
- default: '',
- },
- lineColor: {
- type: String,
- required: true,
- },
- areaColor: {
- type: String,
- required: true,
- },
- currentCoordinates: {
- type: Object,
- required: false,
- default: () => ({ currentX: 0, currentY: 0 }),
- },
- showDot: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- strokeDashArray() {
- if (this.lineStyle === 'dashed') return '3, 1';
- if (this.lineStyle === 'dotted') return '1, 1';
- return null;
- },
- },
-};
-</script>
-<template>
- <g transform="translate(-5, 20)">
- <circle
- v-if="showDot"
- :cx="currentCoordinates.currentX"
- :cy="currentCoordinates.currentY"
- :fill="lineColor"
- :stroke="lineColor"
- class="circle-path"
- r="3"
- />
- <path :d="generatedAreaPath" :fill="areaColor" class="metric-area" />
- <path
- :d="generatedLinePath"
- :stroke="lineColor"
- :stroke-dasharray="strokeDashArray"
- class="metric-line"
- fill="none"
- stroke-width="1"
- />
- </g>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_info.vue b/app/assets/javascripts/monitoring/components/graph/track_info.vue
deleted file mode 100644
index 3464067834f..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/track_info.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<script>
-import { formatRelevantDigits } from '~/lib/utils/number_utils';
-
-export default {
- name: 'TrackInfo',
- props: {
- track: {
- type: Object,
- required: true,
- },
- },
- computed: {
- summaryMetrics() {
- return `Avg: ${formatRelevantDigits(this.track.average)} · Max: ${formatRelevantDigits(
- this.track.max,
- )}`;
- },
- },
-};
-</script>
-<template>
- <span>
- <slot>
- <strong> {{ track.metricTag }} </strong>
- </slot>
- {{ summaryMetrics }}
- </span>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue
deleted file mode 100644
index d2ed1ba113e..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/track_line.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<script>
-export default {
- name: 'TrackLine',
- props: {
- track: {
- type: Object,
- required: true,
- },
- },
- computed: {
- stylizedLine() {
- if (this.track.lineStyle === 'dashed') return '6, 3';
- if (this.track.lineStyle === 'dotted') return '3, 3';
- return null;
- },
- },
-};
-</script>
-<template>
- <td>
- <svg width="16" height="8">
- <line
- :stroke-dasharray="stylizedLine"
- :stroke="track.lineColor"
- :x1="0"
- :x2="16"
- :y1="4"
- :y2="4"
- stroke-width="4"
- />
- </svg>
- </td>
-</template>
diff --git a/app/assets/javascripts/monitoring/event_hub.js b/app/assets/javascripts/monitoring/event_hub.js
deleted file mode 100644
index 0948c2e5352..00000000000
--- a/app/assets/javascripts/monitoring/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import Vue from 'vue';
-
-export default new Vue();
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
deleted file mode 100644
index 87c3d969de4..00000000000
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { bisectDate } from '../utils/date_time_formatters';
-
-const mixins = {
- methods: {
- mouseOverDeployInfo(mouseXPos) {
- if (!this.reducedDeploymentData) return false;
-
- let dataFound = false;
- this.reducedDeploymentData = this.reducedDeploymentData.map(d => {
- const deployment = d;
- if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
- dataFound = d.xPos + 1;
-
- deployment.showDeploymentFlag = true;
- } else {
- deployment.showDeploymentFlag = false;
- }
- return deployment;
- });
-
- return dataFound;
- },
-
- formatDeployments() {
- this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
- const time = new Date(deployment.created_at);
- const xPos = Math.floor(this.timeSeries[0].timeSeriesScaleX(time));
-
- time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
-
- if (xPos >= 0) {
- const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1);
-
- deploymentDataArray.push({
- id: deployment.id,
- time,
- sha: deployment.sha,
- commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
- tag: deployment.tag,
- tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null,
- ref: deployment.ref.name,
- xPos,
- seriesIndex,
- showDeploymentFlag: false,
- });
- }
-
- return deploymentDataArray;
- }, []);
- },
-
- positionFlag() {
- const timeSeries = this.seriesUnderMouse[0];
- if (!timeSeries) {
- return;
- }
- const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate);
-
- this.currentData = timeSeries.values[hoveredDataIndex];
- this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
-
- this.currentCoordinates = {};
-
- this.seriesUnderMouse.forEach(series => {
- const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate);
- const currentData = series.values[currentDataIndex];
- const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
- const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
-
- this.currentCoordinates[series.metricTag] = {
- currentX,
- currentY,
- currentDataIndex,
- };
- });
-
- if (this.hoverData.currentDeployXPos) {
- this.showFlag = false;
- } else {
- this.showFlag = true;
- }
- },
- },
-};
-
-export default mixins;
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 8692c873a41..70635059bd9 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -13,7 +13,7 @@ function checkQueryEmptyData(query) {
result: query.result.filter(timeSeries => {
const newTimeSeries = timeSeries;
const hasValue = series =>
- !Number.isNaN(series.value) && (series.value !== null || series.value !== undefined);
+ !Number.isNaN(series[1]) && (series[1] !== null || series[1] !== undefined);
const hasNonNullValue = timeSeries.values.find(hasValue);
newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : [];
@@ -33,10 +33,10 @@ function normalizeMetrics(metrics) {
...query,
result: query.result.map(result => ({
...result,
- values: result.values.map(([timestamp, value]) => ({
- time: new Date(timestamp * 1000),
- value: Number(value),
- })),
+ values: result.values.map(([timestamp, value]) => [
+ new Date(timestamp * 1000).toISOString(),
+ Number(value),
+ ]),
})),
}));
@@ -66,9 +66,7 @@ export default class MonitoringStore {
}
storeEnvironmentsData(environmentsData = []) {
- this.environmentsData = environmentsData.filter(
- environment => !!environment.latest.last_deployment,
- );
+ this.environmentsData = environmentsData.filter(environment => !!environment.last_deployment);
}
getMetricsCount() {
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
deleted file mode 100644
index d88c13609dc..00000000000
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { timeFormat as time } from 'd3-time-format';
-import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear } from 'd3-time';
-import { bisector } from 'd3-array';
-
-const d3 = {
- time,
- bisector,
- timeSecond,
- timeMinute,
- timeHour,
- timeDay,
- timeWeek,
- timeMonth,
- timeYear,
-};
-
-export const dateFormat = d3.time('%d %b %Y, ');
-export const timeFormat = d3.time('%-I:%M%p');
-export const dateFormatWithName = d3.time('%a, %b %-d');
-export const bisectDate = d3.bisector(d => d.time).left;
-
-export function timeScaleFormat(date) {
- let formatFunction;
- if (d3.timeSecond(date) < date) {
- formatFunction = d3.time('.%L');
- } else if (d3.timeMinute(date) < date) {
- formatFunction = d3.time(':%S');
- } else if (d3.timeHour(date) < date) {
- formatFunction = d3.time('%-I:%M');
- } else if (d3.timeDay(date) < date) {
- formatFunction = d3.time('%-I %p');
- } else if (d3.timeWeek(date) < date) {
- formatFunction = d3.time('%a %d');
- } else if (d3.timeMonth(date) < date) {
- formatFunction = d3.time('%b %d');
- } else if (d3.timeYear(date) < date) {
- formatFunction = d3.time('%B');
- } else {
- formatFunction = d3.time('%Y');
- }
- return formatFunction(date);
-}
diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js
deleted file mode 100644
index 7c771f43eee..00000000000
--- a/app/assets/javascripts/monitoring/utils/measurements.js
+++ /dev/null
@@ -1,44 +0,0 @@
-export default {
- small: {
- // Covers both xs and sm screen sizes
- margin: {
- top: 40,
- right: 40,
- bottom: 50,
- left: 40,
- },
- legends: {
- width: 15,
- height: 3,
- offsetX: 20,
- offsetY: 32,
- },
- backgroundLegend: {
- width: 30,
- height: 50,
- },
- axisLabelLineOffset: -20,
- },
- large: {
- // This covers both md and lg screen sizes
- margin: {
- top: 80,
- right: 80,
- bottom: 100,
- left: 80,
- },
- legends: {
- width: 15,
- height: 3,
- offsetX: 20,
- offsetY: 34,
- },
- backgroundLegend: {
- width: 30,
- height: 150,
- },
- axisLabelLineOffset: 20,
- },
- xTicks: 8,
- yTicks: 3,
-};
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
deleted file mode 100644
index 50ba14dfb2e..00000000000
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ /dev/null
@@ -1,223 +0,0 @@
-import _ from 'underscore';
-import { scaleLinear, scaleTime } from 'd3-scale';
-import { line, area, curveLinear } from 'd3-shape';
-import { extent, max, sum } from 'd3-array';
-import { timeMinute, timeSecond } from 'd3-time';
-import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-
-const d3 = {
- scaleLinear,
- scaleTime,
- line,
- area,
- curveLinear,
- extent,
- max,
- timeMinute,
- timeSecond,
- sum,
-};
-
-const defaultColorPalette = {
- blue: ['#1f78d1', '#8fbce8'],
- orange: ['#fc9403', '#feca81'],
- red: ['#db3b21', '#ed9d90'],
- green: ['#1aaa55', '#8dd5aa'],
- purple: ['#6666c4', '#d1d1f0'],
-};
-
-const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
-
-const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
-
-function queryTimeSeries(query, graphDrawData, lineStyle) {
- let usedColors = [];
- let renderCanary = false;
- const timeSeriesParsed = [];
-
- function pickColor(name) {
- let pick;
- if (name && defaultColorPalette[name]) {
- pick = name;
- } else {
- const unusedColors = _.difference(defaultColorOrder, usedColors);
- if (unusedColors.length > 0) {
- [pick] = unusedColors;
- } else {
- usedColors = [];
- [pick] = defaultColorOrder;
- }
- }
- usedColors.push(pick);
- return defaultColorPalette[pick];
- }
-
- function findByDate(series, time) {
- const val = series.find(v => Math.abs(d3.timeSecond.count(time, v.time)) < 60);
- if (val) {
- return val.value;
- }
- return NaN;
- }
-
- // The timeseries data may have gaps in it
- // but we need a regularly-spaced set of time/value pairs
- // this gives us a complete range of one minute intervals
- // offset the same amount as the original data
- const [minX, maxX] = graphDrawData.xDom;
- const offset = d3.timeMinute(minX) - Number(minX);
- const datesWithoutGaps = d3.timeSecond
- .every(60)
- .range(d3.timeMinute.offset(minX, -1), maxX)
- .map(d => d - offset);
-
- query.result.forEach((timeSeries, timeSeriesNumber) => {
- let metricTag = '';
- let lineColor = '';
- let areaColor = '';
- let shouldRenderLegend = true;
- const timeSeriesValues = timeSeries.values.map(d => d.value);
- const maximumValue = d3.max(timeSeriesValues);
- const accum = d3.sum(timeSeriesValues);
- const trackName = capitalizeFirstCharacter(query.track ? query.track : 'Stable');
-
- if (trackName === 'Canary') {
- renderCanary = true;
- }
-
- const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
- const seriesCustomizationData =
- query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
-
- if (seriesCustomizationData) {
- metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
- [lineColor, areaColor] = pickColor(seriesCustomizationData.color);
- if (timeSeriesParsed.length > 0) {
- shouldRenderLegend = false;
- } else {
- shouldRenderLegend = true;
- }
- } else {
- metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
- [lineColor, areaColor] = pickColor();
- if (timeSeriesParsed.length > 1) {
- shouldRenderLegend = false;
- }
- }
-
- const values = datesWithoutGaps.map(time => ({
- time,
- value: findByDate(timeSeries.values, time),
- }));
-
- timeSeriesParsed.push({
- linePath: graphDrawData.lineFunction(values),
- areaPath: graphDrawData.areaBelowLine(values),
- timeSeriesScaleX: graphDrawData.timeSeriesScaleX,
- timeSeriesScaleY: graphDrawData.timeSeriesScaleY,
- values: timeSeries.values,
- max: maximumValue,
- average: accum / timeSeries.values.length,
- lineStyle,
- lineColor,
- areaColor,
- metricTag,
- trackName,
- shouldRenderLegend,
- renderCanary,
- });
-
- if (!shouldRenderLegend) {
- if (!timeSeriesParsed[0].tracksLegend) {
- timeSeriesParsed[0].tracksLegend = [];
- }
- timeSeriesParsed[0].tracksLegend.push({
- max: maximumValue,
- average: accum / timeSeries.values.length,
- lineStyle,
- lineColor,
- metricTag,
- });
- }
- });
-
- return timeSeriesParsed;
-}
-
-function xyDomain(queries) {
- const allValues = queries.reduce(
- (allQueryResults, query) =>
- allQueryResults.concat(
- query.result.reduce((allResults, result) => allResults.concat(result.values), []),
- ),
- [],
- );
-
- const xDom = d3.extent(allValues, d => d.time);
- const yDom = [0, d3.max(allValues.map(d => d.value))];
-
- return {
- xDom,
- yDom,
- };
-}
-
-export function generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset) {
- const { xDom, yDom } = xyDomain(queries);
-
- const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
- const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]);
-
- timeSeriesScaleX.domain(xDom);
- timeSeriesScaleX.ticks(d3.timeMinute, 60);
- timeSeriesScaleY.domain(yDom);
-
- const defined = d => !Number.isNaN(d.value) && d.value != null;
-
- const lineFunction = d3
- .line()
- .defined(defined)
- .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
- .x(d => timeSeriesScaleX(d.time))
- .y(d => timeSeriesScaleY(d.value));
-
- const areaBelowLine = d3
- .area()
- .defined(defined)
- .curve(d3.curveLinear)
- .x(d => timeSeriesScaleX(d.time))
- .y0(graphHeight - graphHeightOffset)
- .y1(d => timeSeriesScaleY(d.value));
-
- const areaAboveLine = d3
- .area()
- .defined(defined)
- .curve(d3.curveLinear)
- .x(d => timeSeriesScaleX(d.time))
- .y0(0)
- .y1(d => timeSeriesScaleY(d.value));
-
- return {
- lineFunction,
- areaBelowLine,
- areaAboveLine,
- xDom,
- yDom,
- timeSeriesScaleX,
- timeSeriesScaleY,
- };
-}
-
-export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
- const graphDrawData = generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset);
-
- const timeSeries = queries.reduce((series, query, index) => {
- const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length];
- return series.concat(queryTimeSeries(query, graphDrawData, lineStyle));
- }, []);
-
- return {
- timeSeries,
- graphDrawData,
- };
-}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index c3443c300e3..c9c01354333 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1239,15 +1239,13 @@ export default class Notes {
var postUrl = $originalContentEl.data('postUrl');
var targetId = $originalContentEl.data('targetId');
var targetType = $originalContentEl.data('targetType');
- var markdownVersion = $originalContentEl.data('markdownVersion');
this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm
.find('form')
.attr('action', `${postUrl}?html=true`)
- .attr('data-remote', 'true')
- .attr('data-markdown-version', markdownVersion);
+ .attr('data-remote', 'true');
$editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType);
$editForm
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index d669ba5a8fa..1d6cb9485f7 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -39,11 +39,6 @@ export default {
type: String,
required: true,
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
},
data() {
return {
@@ -342,7 +337,6 @@ Please check your network connection and try again.`;
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
- :markdown-version="markdownVersion"
:add-spacing-classes="false"
>
<textarea
diff --git a/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue
new file mode 100644
index 00000000000..ea590905e3c
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue
@@ -0,0 +1,17 @@
+<script>
+export default {
+ name: 'ReplyPlaceholder',
+};
+</script>
+
+<template>
+ <button
+ ref="button"
+ type="button"
+ class="js-vue-discussion-reply btn btn-text-field"
+ :title="s__('MergeRequests|Add a reply')"
+ @click="$emit('onClick')"
+ >
+ {{ s__('MergeRequests|Reply...') }}
+ </button>
+</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index d99694b06e9..cad0d382fa2 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -2,11 +2,13 @@
import { mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import ReplyButton from './note_actions/reply_button.vue';
export default {
name: 'NoteActions',
components: {
Icon,
+ ReplyButton,
GlLoadingIcon,
},
directives: {
@@ -21,6 +23,11 @@ export default {
type: [String, Number],
required: true,
},
+ discussionId: {
+ type: String,
+ required: false,
+ default: '',
+ },
noteUrl: {
type: String,
required: false,
@@ -36,6 +43,10 @@ export default {
required: false,
default: null,
},
+ showReply: {
+ type: Boolean,
+ required: true,
+ },
canEdit: {
type: Boolean,
required: true,
@@ -80,6 +91,9 @@ export default {
},
computed: {
...mapGetters(['getUserDataByProp']),
+ showReplyButton() {
+ return gon.features && gon.features.replyToIndividualNotes && this.showReply;
+ },
shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
},
@@ -137,10 +151,9 @@ export default {
</div>
<div v-if="canAwardEmoji" class="note-actions-item">
<a
- v-gl-tooltip.bottom
+ v-gl-tooltip
:class="{ 'js-user-authored': isAuthoredByCurrentUser }"
class="note-action-button note-emoji-button js-add-award js-note-emoji"
- data-position="right"
href="#"
title="Add reaction"
>
@@ -153,9 +166,15 @@ export default {
<icon css-classes="link-highlight award-control-icon-super-positive" name="emoji_smiley" />
</a>
</div>
+ <reply-button
+ v-if="showReplyButton"
+ ref="replyButton"
+ class="js-reply-button"
+ :note-id="discussionId"
+ />
<div v-if="canEdit" class="note-actions-item">
<button
- v-gl-tooltip.bottom
+ v-gl-tooltip
type="button"
title="Edit comment"
class="note-action-button js-note-edit btn btn-transparent"
@@ -166,7 +185,7 @@ export default {
</div>
<div v-if="showDeleteAction" class="note-actions-item">
<button
- v-gl-tooltip.bottom
+ v-gl-tooltip
type="button"
title="Delete comment"
class="note-action-button js-note-delete btn btn-transparent"
@@ -177,7 +196,7 @@ export default {
</div>
<div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions note-actions-item">
<button
- v-gl-tooltip.bottom
+ v-gl-tooltip
type="button"
title="More actions"
class="note-action-button more-actions-toggle btn btn-transparent"
diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
new file mode 100644
index 00000000000..b2f9d7f128a
--- /dev/null
+++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
@@ -0,0 +1,40 @@
+<script>
+import { mapActions } from 'vuex';
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ name: 'ReplyButton',
+ components: {
+ Icon,
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ noteId: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions(['convertToDiscussion']),
+ },
+};
+</script>
+
+<template>
+ <div class="note-actions-item">
+ <gl-button
+ ref="button"
+ v-gl-tooltip.bottom
+ class="note-action-button"
+ variant="transparent"
+ :title="__('Reply to comment')"
+ @click="convertToDiscussion(noteId)"
+ >
+ <icon name="comment" css-classes="link-highlight" />
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 3efdd1c5c17..17e5fcab5b7 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -171,7 +171,6 @@ export default {
:class="getAwardClassBindings(awardList)"
:title="awardTitle(awardList)"
data-boundary="viewport"
- data-placement="bottom"
class="btn award-control"
type="button"
@click="handleAward(awardName)"
@@ -187,7 +186,6 @@ export default {
title="Add reaction"
aria-label="Add reaction"
data-boundary="viewport"
- data-placement="bottom"
type="button"
>
<span class="award-control-icon award-control-icon-neutral">
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index bcf5d334da4..ff303d0f55a 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -111,7 +111,6 @@ export default {
:line="line"
:note="note"
:help-page-path="helpPagePath"
- :markdown-version="note.cached_markdown_version"
@handleFormUpdate="handleFormUpdate"
@cancelForm="formCancelHandler"
/>
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 269b4a4b117..92258a25438 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -26,11 +26,6 @@ export default {
required: false,
default: '',
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
saveButtonTitle: {
type: String,
required: false,
@@ -202,7 +197,6 @@ export default {
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
- :markdown-version="markdownVersion"
:quick-actions-docs-path="quickActionsDocsPath"
:line="line"
:note="discussionNote"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 695efe3602f..b7e9f7c2028 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -24,6 +24,7 @@ import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import discussionNavigation from '../mixins/discussion_navigation';
+import ReplyPlaceholder from './discussion_reply_placeholder.vue';
import jumpToNextDiscussionButton from './discussion_jump_to_next_button.vue';
export default {
@@ -39,6 +40,7 @@ export default {
resolveDiscussionButton,
jumpToNextDiscussionButton,
toggleRepliesWidget,
+ ReplyPlaceholder,
placeholderNote,
placeholderSystemNote,
systemNote,
@@ -396,6 +398,7 @@ Please check your network connection and try again.`;
:line="line"
:commit="commit"
:help-page-path="helpPagePath"
+ :show-reply-button="canReply"
@handleDeleteNote="deleteNoteHandler"
>
<note-edited-text
@@ -447,14 +450,7 @@ Please check your network connection and try again.`;
>
<template v-if="!isReplying && canReply">
<div class="discussion-with-resolve-btn">
- <button
- type="button"
- class="js-vue-discussion-reply btn btn-text-field qa-discussion-reply"
- title="Add a reply"
- @click="showReplyForm"
- >
- Reply...
- </button>
+ <reply-placeholder class="qa-discussion-reply" @onClick="showReplyForm" />
<resolve-discussion-button
v-if="discussion.resolvable"
:is-resolving="isResolving"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 3c48d81ed05..56108a58010 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -29,6 +29,11 @@ export default {
type: Object,
required: true,
},
+ discussion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
line: {
type: Object,
required: false,
@@ -54,7 +59,7 @@ export default {
};
},
computed: {
- ...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData']),
+ ...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData', 'commentsDisabled']),
author() {
return this.note.author;
},
@@ -80,6 +85,19 @@ export default {
isTarget() {
return this.targetNoteHash === this.noteAnchorId;
},
+ discussionId() {
+ if (this.discussion) {
+ return this.discussion.id;
+ }
+ return '';
+ },
+ showReplyButton() {
+ if (!this.discussion || !this.getNoteableData.current_user.can_create_note) {
+ return false;
+ }
+
+ return this.discussion.individual_note && !this.commentsDisabled;
+ },
actionText() {
if (!this.commit) {
return '';
@@ -231,6 +249,7 @@ export default {
:note-id="note.id"
:note-url="note.noteable_note_url"
:access-level="note.human_access"
+ :show-reply="showReplyButton"
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
@@ -241,6 +260,7 @@ export default {
:is-resolved="note.resolved"
:is-resolving="isResolving"
:resolved-by="note.resolved_by"
+ :discussion-id="discussionId"
@handleEdit="editHandler"
@handleDelete="deleteHandler"
@handleResolve="resolveHandler"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index f3fcfdfda05..6d72b72e628 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -44,11 +44,6 @@ export default {
required: false,
default: true,
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
helpPagePath: {
type: String,
required: false,
@@ -204,7 +199,12 @@ export default {
:key="discussion.id"
:note="discussion.notes[0]"
/>
- <noteable-note v-else :key="discussion.id" :note="discussion.notes[0]" />
+ <noteable-note
+ v-else
+ :key="discussion.id"
+ :note="discussion.notes[0]"
+ :discussion="discussion"
+ />
</template>
<noteable-discussion
v-else
@@ -216,10 +216,6 @@ export default {
</template>
</ul>
- <comment-form
- v-if="!commentsDisabled"
- :noteable-type="noteableType"
- :markdown-version="markdownVersion"
- />
+ <comment-form v-if="!commentsDisabled" :noteable-type="noteableType" />
</div>
</template>
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 2f715c85fa6..4883266dae5 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -18,7 +18,6 @@ document.addEventListener('DOMContentLoaded', () => {
const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const noteableData = JSON.parse(notesDataset.noteableData);
- const markdownVersion = parseInt(notesDataset.markdownVersion, 10);
let currentUserData = {};
noteableData.noteableType = notesDataset.noteableType;
@@ -37,7 +36,6 @@ document.addEventListener('DOMContentLoaded', () => {
return {
noteableData,
currentUserData,
- markdownVersion,
notesData: JSON.parse(notesDataset.notesData),
};
},
@@ -47,7 +45,6 @@ document.addEventListener('DOMContentLoaded', () => {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
- markdownVersion: this.markdownVersion,
},
});
},
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 2105a62cecb..ff65f14d529 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -426,5 +426,8 @@ export const submitSuggestion = (
});
};
+export const convertToDiscussion = ({ commit }, noteId) =>
+ commit(types.CONVERT_TO_DISCUSSION, noteId);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index df943c155f4..2bffedad336 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -17,6 +17,7 @@ export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
export const DISABLE_COMMENTS = 'DISABLE_COMMENTS';
export const APPLY_SUGGESTION = 'APPLY_SUGGESTION';
+export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 33d39ad2ec9..d167f8ef421 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -264,4 +264,9 @@ export default {
).length;
state.hasUnresolvedDiscussions = state.unresolvedDiscussionsCount > 1;
},
+
+ [types.CONVERT_TO_DISCUSSION](state, discussionId) {
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
+ Object.assign(discussion, { individual_note: false });
+ },
};
diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index.js
index 8f98be79640..01001d4f3ff 100644
--- a/app/assets/javascripts/pages/dashboard/projects/index.js
+++ b/app/assets/javascripts/pages/dashboard/projects/index.js
@@ -1,7 +1,5 @@
import ProjectsList from '~/projects_list';
-import Star from '../../../star';
document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
- new Star('.project-row'); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js
index 8f98be79640..01001d4f3ff 100644
--- a/app/assets/javascripts/pages/explore/projects/index.js
+++ b/app/assets/javascripts/pages/explore/projects/index.js
@@ -1,7 +1,5 @@
import ProjectsList from '~/projects_list';
-import Star from '../../../star';
document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
- new Star('.project-row'); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index c7ce4675573..0dd0d5336fc 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import createFlash from '~/flash';
import GfmAutoComplete from '~/gfm_auto_complete';
+import emojiRegex from 'emoji-regex';
import EmojiMenu from './emoji_menu';
const defaultStatusEmoji = 'speech_balloon';
@@ -42,6 +43,17 @@ document.addEventListener('DOMContentLoaded', () => {
const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(statusMessageField), { emojis: true });
+ const userNameInput = document.getElementById('user_name');
+ userNameInput.addEventListener('input', () => {
+ const EMOJI_REGEX = emojiRegex();
+ if (EMOJI_REGEX.test(userNameInput.value)) {
+ // set field to invalid so it gets detected by GlFieldErrors
+ userNameInput.setCustomValidity('Invalid field');
+ } else {
+ userNameInput.setCustomValidity('');
+ }
+ });
+
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(Emoji => {
const emojiMenu = new EmojiMenu(
diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js
index 0b644780ad4..0d69a689316 100644
--- a/app/assets/javascripts/pages/projects/environments/metrics/index.js
+++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js
@@ -1,3 +1,3 @@
-import monitoringBundle from '~/monitoring/monitoring_bundle';
+import monitoringBundle from 'ee_else_ce/monitoring/monitoring_bundle';
document.addEventListener('DOMContentLoaded', monitoringBundle);
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index 3ccad513c05..26d7fa7371d 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -43,10 +43,26 @@ document.addEventListener('DOMContentLoaded', () => {
],
});
+ const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
+ if (firstDayOfWeek === 0) {
+ return weekDays;
+ }
+
+ return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
+ const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
+
+ return {
+ ...acc,
+ [reorderedDayName]: weekDays[reorderedDayName],
+ };
+ }, {});
+ };
+
const hourData = chartData(projectChartData.hour);
responsiveChart($('#hour-chart'), hourData);
- const dayData = chartData(projectChartData.weekDays);
+ const weekDays = reorderWeekDays(projectChartData.weekDays, gon.first_day_of_week);
+ const dayData = chartData(weekDays);
responsiveChart($('#weekday-chart'), dayData);
const monthData = chartData(projectChartData.month);
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 8a84ac37dab..afa099d0e0b 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -159,7 +159,7 @@ export default class ActivityCalendar {
.append('g')
.attr('transform', (group, i) => {
_.each(group, (stamp, a) => {
- if (a === 0 && stamp.day === 0) {
+ if (a === 0 && stamp.day === this.firstDayOfWeek) {
const month = stamp.date.getMonth();
const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months);
@@ -205,6 +205,14 @@ export default class ActivityCalendar {
y: 29 + this.dayYPos(5),
},
];
+
+ if (this.firstDayOfWeek === 1) {
+ days.push({
+ text: 'S',
+ y: 29 + this.dayYPos(7),
+ });
+ }
+
this.svg
.append('g')
.selectAll('text')
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 1c3fd58ca74..39cd891c111 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -234,7 +234,7 @@ export default class UserTabs {
data,
calendarActivitiesPath,
utcOffset,
- 0,
+ gon.first_day_of_week,
monthsAgo,
);
}
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index cf9db89e32b..2b32a6e4a98 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -108,7 +108,7 @@ export default {
:href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
- class="js-pipeline-graph-job-link"
+ class="js-pipeline-graph-job-link qa-job-link"
>
<job-name-component :name="job.name" :status="job.status" />
</gl-link>
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index a33835472bb..5ee510eb11d 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -5,97 +5,101 @@ import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
export default function projectSelect() {
- $('.ajax-project-select').each(function(i, select) {
- var placeholder;
- const simpleFilter = $(select).data('simpleFilter') || false;
- this.groupId = $(select).data('groupId');
- this.includeGroups = $(select).data('includeGroups');
- this.allProjects = $(select).data('allProjects') || false;
- this.orderBy = $(select).data('orderBy') || 'id';
- this.withIssuesEnabled = $(select).data('withIssuesEnabled');
- this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
- this.withShared =
- $(select).data('withShared') === undefined ? true : $(select).data('withShared');
- this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
- this.allowClear = $(select).data('allowClear') || false;
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('.ajax-project-select').each(function(i, select) {
+ var placeholder;
+ const simpleFilter = $(select).data('simpleFilter') || false;
+ this.groupId = $(select).data('groupId');
+ this.includeGroups = $(select).data('includeGroups');
+ this.allProjects = $(select).data('allProjects') || false;
+ this.orderBy = $(select).data('orderBy') || 'id';
+ this.withIssuesEnabled = $(select).data('withIssuesEnabled');
+ this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
+ this.withShared =
+ $(select).data('withShared') === undefined ? true : $(select).data('withShared');
+ this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
+ this.allowClear = $(select).data('allowClear') || false;
- placeholder = 'Search for project';
- if (this.includeGroups) {
- placeholder += ' or group';
- }
+ placeholder = 'Search for project';
+ if (this.includeGroups) {
+ placeholder += ' or group';
+ }
- $(select).select2({
- placeholder: placeholder,
- minimumInputLength: 0,
- query: (function(_this) {
- return function(query) {
- var finalCallback, projectsCallback;
- finalCallback = function(projects) {
- var data;
- data = {
- results: projects,
- };
- return query.callback(data);
- };
- if (_this.includeGroups) {
- projectsCallback = function(projects) {
- var groupsCallback;
- groupsCallback = function(groups) {
+ $(select).select2({
+ placeholder: placeholder,
+ minimumInputLength: 0,
+ query: (function(_this) {
+ return function(query) {
+ var finalCallback, projectsCallback;
+ finalCallback = function(projects) {
var data;
- data = groups.concat(projects);
- return finalCallback(data);
+ data = {
+ results: projects,
+ };
+ return query.callback(data);
};
- return Api.groups(query.term, {}, groupsCallback);
+ if (_this.includeGroups) {
+ projectsCallback = function(projects) {
+ var groupsCallback;
+ groupsCallback = function(groups) {
+ var data;
+ data = groups.concat(projects);
+ return finalCallback(data);
+ };
+ return Api.groups(query.term, {}, groupsCallback);
+ };
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (_this.groupId) {
+ return Api.groupProjects(
+ _this.groupId,
+ query.term,
+ {
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ with_shared: _this.withShared,
+ include_subgroups: _this.includeProjectsInSubgroups,
+ },
+ projectsCallback,
+ );
+ } else {
+ return Api.projects(
+ query.term,
+ {
+ order_by: _this.orderBy,
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ membership: !_this.allProjects,
+ },
+ projectsCallback,
+ );
+ }
};
- } else {
- projectsCallback = finalCallback;
- }
- if (_this.groupId) {
- return Api.groupProjects(
- _this.groupId,
- query.term,
- {
- with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled,
- with_shared: _this.withShared,
- include_subgroups: _this.includeProjectsInSubgroups,
- },
- projectsCallback,
- );
- } else {
- return Api.projects(
- query.term,
- {
- order_by: _this.orderBy,
- with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled,
- membership: !_this.allProjects,
- },
- projectsCallback,
- );
- }
- };
- })(this),
- id: function(project) {
- if (simpleFilter) return project.id;
- return JSON.stringify({
- name: project.name,
- url: project.web_url,
- });
- },
- text: function(project) {
- return project.name_with_namespace || project.name;
- },
+ })(this),
+ id: function(project) {
+ if (simpleFilter) return project.id;
+ return JSON.stringify({
+ name: project.name,
+ url: project.web_url,
+ });
+ },
+ text: function(project) {
+ return project.name_with_namespace || project.name;
+ },
- initSelection: function(el, callback) {
- return Api.project(el.val()).then(({ data }) => callback(data));
- },
+ initSelection: function(el, callback) {
+ return Api.project(el.val()).then(({ data }) => callback(data));
+ },
- allowClear: this.allowClear,
+ allowClear: this.allowClear,
- dropdownCssClass: 'ajax-project-dropdown',
- });
- if (simpleFilter) return select;
- return new ProjectSelectComboButton(select);
- });
+ dropdownCssClass: 'ajax-project-dropdown',
+ });
+ if (simpleFilter) return select;
+ return new ProjectSelectComboButton(select);
+ });
+ })
+ .catch(() => {});
}
diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js
index 3dbac3ff942..d3b5f532dc1 100644
--- a/app/assets/javascripts/project_select_combo_button.js
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -44,9 +44,13 @@ export default class ProjectSelectComboButton {
// eslint-disable-next-line class-methods-use-this
openDropdown(event) {
- $(event.currentTarget)
- .siblings('.project-item-select')
- .select2('open');
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $(event.currentTarget)
+ .siblings('.project-item-select')
+ .select2('open');
+ })
+ .catch(() => {});
}
selectProject() {
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 998554d1be5..d65e73a3f9c 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -115,15 +115,35 @@ const bindEvents = () => {
const templates = {
rails: {
text: 'Ruby on Rails',
- icon: '.template-option svg.icon-rails',
+ icon: '.template-option .icon-rails',
},
express: {
text: 'NodeJS Express',
- icon: '.template-option svg.icon-node-express',
+ icon: '.template-option .icon-express',
},
spring: {
text: 'Spring',
- icon: '.template-option svg.icon-java-spring',
+ icon: '.template-option .icon-spring',
+ },
+ hugo: {
+ text: 'Pages/Hugo',
+ icon: '.template-option .icon-hugo',
+ },
+ jekyll: {
+ text: 'Pages/Jekyll',
+ icon: '.template-option .icon-jekyll',
+ },
+ plainhtml: {
+ text: 'Pages/Plain HTML',
+ icon: '.template-option .icon-plainhtml',
+ },
+ gitbook: {
+ text: 'Pages/GitBook',
+ icon: '.template-option .icon-gitbook',
+ },
+ hexo: {
+ text: 'Pages/Hexo',
+ icon: '.template-option .icon-hexo',
},
};
diff --git a/app/assets/javascripts/serverless/components/environment_row.vue b/app/assets/javascripts/serverless/components/environment_row.vue
new file mode 100644
index 00000000000..4d18c5c4bdd
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/environment_row.vue
@@ -0,0 +1,65 @@
+<script>
+import FunctionRow from './function_row.vue';
+import ItemCaret from '~/groups/components/item_caret.vue';
+
+export default {
+ components: {
+ ItemCaret,
+ FunctionRow,
+ },
+ props: {
+ env: {
+ type: Array,
+ required: true,
+ },
+ envName: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isOpen: true,
+ };
+ },
+ computed: {
+ envId() {
+ if (this.envName === '*') {
+ return 'env-global';
+ }
+
+ return `env-${this.envName}`;
+ },
+ isOpenClass() {
+ return {
+ 'is-open': this.isOpen,
+ };
+ },
+ },
+ methods: {
+ toggleOpen() {
+ this.isOpen = !this.isOpen;
+ },
+ },
+};
+</script>
+
+<template>
+ <li :id="envId" :class="isOpenClass" class="group-row has-children">
+ <div
+ class="group-row-contents d-flex justify-content-end align-items-center"
+ role="button"
+ @click.stop="toggleOpen"
+ >
+ <div class="folder-toggle-wrap d-flex align-items-center">
+ <item-caret :is-group-open="isOpen" />
+ </div>
+ <div class="group-text flex-grow title namespace-title prepend-left-default">
+ {{ envName }}
+ </div>
+ </div>
+ <ul v-if="isOpen" class="content-list group-list-tree">
+ <function-row v-for="(f, index) in env" :key="f.name" :index="index" :func="f" />
+ </ul>
+ </li>
+</template>
diff --git a/app/assets/javascripts/serverless/components/function_details.vue b/app/assets/javascripts/serverless/components/function_details.vue
index 2b1c21f041b..4f89ad69129 100644
--- a/app/assets/javascripts/serverless/components/function_details.vue
+++ b/app/assets/javascripts/serverless/components/function_details.vue
@@ -1,13 +1,11 @@
<script>
import PodBox from './pod_box.vue';
-import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import Icon from '~/vue_shared/components/icon.vue';
+import Url from './url.vue';
export default {
components: {
- Icon,
PodBox,
- ClipboardButton,
+ Url,
},
props: {
func: {
@@ -36,24 +34,9 @@ export default {
<section id="serverless-function-details">
<h3>{{ name }}</h3>
<div class="append-bottom-default">
- <div v-for="line in description.split('\n')" :key="line">{{ line }}<br /></div>
- </div>
- <div class="clipboard-group append-bottom-default">
- <div class="label label-monospace">{{ funcUrl }}</div>
- <clipboard-button
- :text="String(funcUrl)"
- :title="s__('ServerlessDetails|Copy URL to clipboard')"
- class="input-group-text js-clipboard-btn"
- />
- <a
- :href="funcUrl"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="input-group-text btn btn-default"
- >
- <icon name="external-link" />
- </a>
+ <div v-for="(line, index) in description.split('\n')" :key="index">{{ line }}</div>
</div>
+ <url :uri="funcUrl" />
<h4>{{ s__('ServerlessDetails|Kubernetes Pods') }}</h4>
<div v-if="podCount > 0">
diff --git a/app/assets/javascripts/serverless/components/function_row.vue b/app/assets/javascripts/serverless/components/function_row.vue
index 44bfae388cb..773d18781fd 100644
--- a/app/assets/javascripts/serverless/components/function_row.vue
+++ b/app/assets/javascripts/serverless/components/function_row.vue
@@ -1,9 +1,12 @@
<script>
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
+import Url from './url.vue';
+import { visitUrl } from '~/lib/utils/url_utility';
export default {
components: {
Timeago,
+ Url,
},
props: {
func: {
@@ -16,13 +19,18 @@ export default {
return this.func.name;
},
description() {
- return this.func.description;
+ const desc = this.func.description.split('\n');
+ if (desc.length > 1) {
+ return desc[1];
+ }
+
+ return desc[0];
},
detailUrl() {
return this.func.detail_url;
},
- environment() {
- return this.func.environment_scope;
+ targetUrl() {
+ return this.func.url;
},
image() {
return this.func.image;
@@ -31,25 +39,34 @@ export default {
return this.func.created_at;
},
},
+ methods: {
+ checkClass(element) {
+ if (element.closest('.no-expand') === null) {
+ return true;
+ }
+
+ return false;
+ },
+ openDetails(e) {
+ if (this.checkClass(e.target)) {
+ visitUrl(this.detailUrl);
+ }
+ },
+ },
};
</script>
<template>
- <div class="gl-responsive-table-row">
- <div class="table-section section-20 section-wrap">
- <a :href="detailUrl">{{ name }}</a>
- </div>
- <div class="table-section section-10">{{ environment }}</div>
- <div class="table-section section-40 section-wrap">
- <span class="line-break">{{ description }}</span>
+ <li :id="name" class="group-row">
+ <div class="group-row-contents" role="button" @click="openDetails">
+ <p class="float-right text-right">
+ <span>{{ image }}</span
+ ><br />
+ <timeago :time="timestamp" />
+ </p>
+ <b>{{ name }}</b>
+ <div v-for="line in description.split('\n')" :key="line">{{ line }}</div>
+ <url :uri="targetUrl" class="prepend-top-8 no-expand" />
</div>
- <div class="table-section section-20">{{ image }}</div>
- <div class="table-section section-10"><timeago :time="timestamp" /></div>
- </div>
+ </li>
</template>
-
-<style>
-.line-break {
- white-space: pre;
-}
-</style>
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index 9606a78410e..4bde409f906 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -1,19 +1,21 @@
<script>
import { GlSkeletonLoading } from '@gitlab/ui';
import FunctionRow from './function_row.vue';
+import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue';
export default {
components: {
+ EnvironmentRow,
FunctionRow,
EmptyState,
GlSkeletonLoading,
},
props: {
functions: {
- type: Array,
+ type: Object,
required: true,
- default: () => [],
+ default: () => ({}),
},
installed: {
type: Boolean,
@@ -45,33 +47,21 @@ export default {
<section id="serverless-functions">
<div v-if="installed">
<div v-if="hasFunctionData">
- <div class="ci-table js-services-list function-element">
- <div class="gl-responsive-table-row table-row-header" role="row">
- <div class="table-section section-20" role="rowheader">
- {{ s__('Serverless|Function') }}
- </div>
- <div class="table-section section-10" role="rowheader">
- {{ s__('Serverless|Cluster Env') }}
- </div>
- <div class="table-section section-40" role="rowheader">
- {{ s__('Serverless|Description') }}
- </div>
- <div class="table-section section-20" role="rowheader">
- {{ s__('Serverless|Runtime') }}
- </div>
- <div class="table-section section-10" role="rowheader">
- {{ s__('Serverless|Last Update') }}
- </div>
+ <template v-if="loadingData">
+ <div v-for="j in 3" :key="j" class="gl-responsive-table-row"><gl-skeleton-loading /></div>
+ </template>
+ <template v-else>
+ <div class="groups-list-tree-container">
+ <ul class="content-list group-list-tree">
+ <environment-row
+ v-for="(env, index) in functions"
+ :key="index"
+ :env="env"
+ :env-name="index"
+ />
+ </ul>
</div>
- <template v-if="loadingData">
- <div v-for="j in 3" :key="j" class="gl-responsive-table-row">
- <gl-skeleton-loading />
- </div>
- </template>
- <template v-else>
- <function-row v-for="f in functions" :key="f.name" :func="f" />
- </template>
- </div>
+ </template>
</div>
<div v-else class="empty-state js-empty-state">
<div class="text-content">
@@ -111,16 +101,3 @@ export default {
<empty-state v-else :clusters-path="clustersPath" :help-path="helpPath" />
</section>
</template>
-
-<style>
-.top-area {
- border-bottom: 0;
-}
-
-.function-element {
- border-bottom: 1px solid #e5e5e5;
- border-bottom-color: rgb(229, 229, 229);
- border-bottom-style: solid;
- border-bottom-width: 1px;
-}
-</style>
diff --git a/app/assets/javascripts/serverless/components/url.vue b/app/assets/javascripts/serverless/components/url.vue
new file mode 100644
index 00000000000..ca53bf6c52a
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/url.vue
@@ -0,0 +1,38 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ GlButton,
+ ClipboardButton,
+ },
+ props: {
+ uri: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="clipboard-group">
+ <div class="url-text-field label label-monospace">{{ uri }}</div>
+ <clipboard-button
+ :text="uri"
+ :title="s__('ServerlessURL|Copy URL to clipboard')"
+ class="input-group-text js-clipboard-btn"
+ />
+ <gl-button
+ :href="uri"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="input-group-text btn btn-default"
+ >
+ <icon name="external-link" />
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/serverless/stores/serverless_store.js b/app/assets/javascripts/serverless/stores/serverless_store.js
index 774c15b5b12..816d55a03f9 100644
--- a/app/assets/javascripts/serverless/stores/serverless_store.js
+++ b/app/assets/javascripts/serverless/stores/serverless_store.js
@@ -1,7 +1,7 @@
export default class ServerlessStore {
constructor(knativeInstalled = false, clustersPath, helpPath) {
this.state = {
- functions: [],
+ functions: {},
hasFunctionData: true,
loadingData: true,
installed: knativeInstalled,
@@ -10,8 +10,13 @@ export default class ServerlessStore {
};
}
- updateFunctionsFromServer(functions = []) {
- this.state.functions = functions;
+ updateFunctionsFromServer(upstreamFunctions = []) {
+ this.state.functions = upstreamFunctions.reduce((rv, func) => {
+ const envs = rv;
+ envs[func.environment_scope] = (rv[func.environment_scope] || []).concat([func]);
+
+ return envs;
+ }, {});
}
updateLoadingState(loadingData) {
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index edefb3735d7..5172a1ef3d6 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import 'deckar01-task_list';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import Flash from './flash';
@@ -8,46 +9,79 @@ export default class TaskList {
this.selector = options.selector;
this.dataType = options.dataType;
this.fieldName = options.fieldName;
+ this.lockVersion = options.lockVersion;
+ this.taskListContainerSelector = `${this.selector} .js-task-list-container`;
+ this.updateHandler = this.update.bind(this);
this.onSuccess = options.onSuccess || (() => {});
- this.onError = function showFlash(e) {
- let errorMessages = '';
+ this.onError =
+ options.onError ||
+ function showFlash(e) {
+ let errorMessages = '';
- if (e.response.data && typeof e.response.data === 'object') {
- errorMessages = e.response.data.errors.join(' ');
- }
+ if (e.response.data && typeof e.response.data === 'object') {
+ errorMessages = e.response.data.errors.join(' ');
+ }
- return new Flash(errorMessages || 'Update failed', 'alert');
- };
+ return new Flash(errorMessages || __('Update failed'), 'alert');
+ };
this.init();
}
init() {
- // Prevent duplicate event bindings
- this.disable();
- $(`${this.selector} .js-task-list-container`).taskList('enable');
- $(document).on(
- 'tasklist:changed',
- `${this.selector} .js-task-list-container`,
- this.update.bind(this),
- );
+ this.disable(); // Prevent duplicate event bindings
+
+ $(this.taskListContainerSelector).taskList('enable');
+ $(document).on('tasklist:changed', this.taskListContainerSelector, this.updateHandler);
+ }
+
+ getTaskListTarget(e) {
+ return e && e.currentTarget ? $(e.currentTarget) : $(this.taskListContainerSelector);
+ }
+
+ disableTaskListItems(e) {
+ this.getTaskListTarget(e).taskList('disable');
+ }
+
+ enableTaskListItems(e) {
+ this.getTaskListTarget(e).taskList('enable');
}
disable() {
- $(`${this.selector} .js-task-list-container`).taskList('disable');
- $(document).off('tasklist:changed', `${this.selector} .js-task-list-container`);
+ this.disableTaskListItems();
+ $(document).off('tasklist:changed', this.taskListContainerSelector);
}
update(e) {
const $target = $(e.target);
+ const { index, checked, lineNumber, lineSource } = e.detail;
const patchData = {};
+
patchData[this.dataType] = {
[this.fieldName]: $target.val(),
+ lock_version: this.lockVersion,
+ update_task: {
+ index,
+ checked,
+ line_number: lineNumber,
+ line_source: lineSource,
+ },
};
+ this.disableTaskListItems(e);
+
return axios
.patch($target.data('updateUrl') || $('form.js-issuable-update').attr('action'), patchData)
- .then(({ data }) => this.onSuccess(data))
- .catch(err => this.onError(err));
+ .then(({ data }) => {
+ this.lockVersion = data.lock_version;
+ this.enableTaskListItems(e);
+
+ return this.onSuccess(data);
+ })
+ .catch(({ response }) => {
+ this.enableTaskListItems(e);
+
+ return this.onError(response.data);
+ });
}
}
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index ce051582299..4017630d6ef 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -579,101 +579,109 @@ function UsersSelect(currentUser, els, options = {}) {
};
})(this),
);
- $('.ajax-users-select').each(
- (function(_this) {
- return function(i, select) {
- var firstUser, showAnyUser, showEmailUser, showNullUser;
- var options = {};
- options.skipLdap = $(select).hasClass('skip_ldap');
- options.projectId = $(select).data('projectId');
- options.groupId = $(select).data('groupId');
- options.showCurrentUser = $(select).data('currentUser');
- options.authorId = $(select).data('authorId');
- options.skipUsers = $(select).data('skipUsers');
- showNullUser = $(select).data('nullUser');
- showAnyUser = $(select).data('anyUser');
- showEmailUser = $(select).data('emailUser');
- firstUser = $(select).data('firstUser');
- return $(select).select2({
- placeholder: 'Search for a user',
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query: function(query) {
- return _this.users(query.term, options, function(users) {
- var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
- data = {
- results: users,
- };
- if (query.term.length === 0) {
- if (firstUser) {
- // Move current user to the front of the list
- ref = data.results;
-
- for (index = 0, len = ref.length; index < len; index += 1) {
- obj = ref[index];
- if (obj.username === firstUser) {
- data.results.splice(index, 1);
- data.results.unshift(obj);
- break;
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('.ajax-users-select').each(
+ (function(_this) {
+ return function(i, select) {
+ var firstUser, showAnyUser, showEmailUser, showNullUser;
+ var options = {};
+ options.skipLdap = $(select).hasClass('skip_ldap');
+ options.projectId = $(select).data('projectId');
+ options.groupId = $(select).data('groupId');
+ options.showCurrentUser = $(select).data('currentUser');
+ options.authorId = $(select).data('authorId');
+ options.skipUsers = $(select).data('skipUsers');
+ showNullUser = $(select).data('nullUser');
+ showAnyUser = $(select).data('anyUser');
+ showEmailUser = $(select).data('emailUser');
+ firstUser = $(select).data('firstUser');
+ return $(select).select2({
+ placeholder: 'Search for a user',
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query: function(query) {
+ return _this.users(query.term, options, function(users) {
+ var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
+ data = {
+ results: users,
+ };
+ if (query.term.length === 0) {
+ if (firstUser) {
+ // Move current user to the front of the list
+ ref = data.results;
+
+ for (index = 0, len = ref.length; index < len; index += 1) {
+ obj = ref[index];
+ if (obj.username === firstUser) {
+ data.results.splice(index, 1);
+ data.results.unshift(obj);
+ break;
+ }
+ }
+ }
+ if (showNullUser) {
+ nullUser = {
+ name: 'Unassigned',
+ id: 0,
+ };
+ data.results.unshift(nullUser);
+ }
+ if (showAnyUser) {
+ name = showAnyUser;
+ if (name === true) {
+ name = 'Any User';
+ }
+ anyUser = {
+ name: name,
+ id: null,
+ };
+ data.results.unshift(anyUser);
}
}
- }
- if (showNullUser) {
- nullUser = {
- name: 'Unassigned',
- id: 0,
- };
- data.results.unshift(nullUser);
- }
- if (showAnyUser) {
- name = showAnyUser;
- if (name === true) {
- name = 'Any User';
+ if (
+ showEmailUser &&
+ data.results.length === 0 &&
+ query.term.match(/^[^@]+@[^@]+$/)
+ ) {
+ var trimmed = query.term.trim();
+ emailUser = {
+ name: 'Invite "' + trimmed + '" by email',
+ username: trimmed,
+ id: trimmed,
+ invite: true,
+ };
+ data.results.unshift(emailUser);
}
- anyUser = {
- name: name,
- id: null,
- };
- data.results.unshift(anyUser);
- }
- }
- if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
- var trimmed = query.term.trim();
- emailUser = {
- name: 'Invite "' + trimmed + '" by email',
- username: trimmed,
- id: trimmed,
- invite: true,
- };
- data.results.unshift(emailUser);
- }
- return query.callback(data);
+ return query.callback(data);
+ });
+ },
+ initSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.initSelection.apply(_this, args);
+ },
+ formatResult: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.formatResult.apply(_this, args);
+ },
+ formatSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.formatSelection.apply(_this, args);
+ },
+ dropdownCssClass: 'ajax-users-dropdown',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup: function(m) {
+ return m;
+ },
});
- },
- initSelection: function() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return _this.initSelection.apply(_this, args);
- },
- formatResult: function() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return _this.formatResult.apply(_this, args);
- },
- formatSelection: function() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return _this.formatSelection.apply(_this, args);
- },
- dropdownCssClass: 'ajax-users-dropdown',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup: function(m) {
- return m;
- },
- });
- };
- })(this),
- );
+ };
+ })(this),
+ );
+ })
+ .catch(() => {});
}
UsersSelect.prototype.initSelection = function(element, callback) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
new file mode 100644
index 00000000000..a38f25cce35
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
@@ -0,0 +1,40 @@
+<script>
+export default {
+ props: {
+ value: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ inputId: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <li>
+ <div class="commit-message-editor">
+ <div class="d-flex flex-wrap align-items-center justify-content-between">
+ <label class="col-form-label" :for="inputId">
+ <strong>{{ label }}</strong>
+ </label>
+ <slot name="header"></slot>
+ </div>
+ <textarea
+ :id="inputId"
+ :value="value"
+ class="form-control js-gfm-input append-bottom-default commit-message-edit"
+ required="required"
+ rows="7"
+ @input="$emit('input', $event.target.value)"
+ ></textarea>
+ <slot name="checkbox"></slot>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue
new file mode 100644
index 00000000000..b3c1c0e329d
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue
@@ -0,0 +1,38 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ props: {
+ commits: {
+ type: Array,
+ required: true,
+ default: () => [],
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-dropdown
+ right
+ no-caret
+ text="Use an existing commit message"
+ variant="link"
+ class="mr-commit-dropdown"
+ >
+ <gl-dropdown-item
+ v-for="commit in commits"
+ :key="commit.short_id"
+ class="text-nowrap text-truncate"
+ @click="$emit('input', commit.message)"
+ >
+ <span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
new file mode 100644
index 00000000000..a1d3a09cca4
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import _ from 'underscore';
+import { __, n__, sprintf, s__ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ GlButton,
+ },
+ props: {
+ isSquashEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ commitsCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ targetBranch: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ expanded: false,
+ };
+ },
+ computed: {
+ collapseIcon() {
+ return this.expanded ? 'chevron-down' : 'chevron-right';
+ },
+ commitsCountMessage() {
+ return n__(__('%d commit'), __('%d commits'), this.isSquashEnabled ? 1 : this.commitsCount);
+ },
+ modifyLinkMessage() {
+ return this.isSquashEnabled ? __('Modify commit messages') : __('Modify merge commit');
+ },
+ ariaLabel() {
+ return this.expanded ? __('Collapse') : __('Expand');
+ },
+ message() {
+ return sprintf(
+ s__(
+ 'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}.',
+ ),
+ {
+ commitCount: `<strong class="commits-count-message">${this.commitsCountMessage}</strong>`,
+ mergeCommitCount: `<strong>${s__('mrWidgetCommitsAdded|1 merge commit')}</strong>`,
+ targetBranch: `<span class="label-branch">${_.escape(this.targetBranch)}</span>`,
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ toggle() {
+ this.expanded = !this.expanded;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="js-mr-widget-commits-count mr-widget-extension clickable d-flex align-items-center px-3 py-2"
+ @click="toggle()"
+ >
+ <gl-button
+ :aria-label="ariaLabel"
+ variant="blank"
+ class="commit-edit-toggle mr-2"
+ @click.stop="toggle()"
+ >
+ <icon :name="collapseIcon" :size="16" />
+ </gl-button>
+ <span v-if="expanded">{{ __('Collapse') }}</span>
+ <span v-else>
+ <span v-html="message"></span>
+ <gl-button variant="link" class="modify-message-button">
+ {{ modifyLinkMessage }}
+ </gl-button>
+ </span>
+ </div>
+ <div v-show="expanded"><slot></slot></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index b8f29649eb5..ce4207864ea 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
@@ -2,17 +2,24 @@
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
+import { __ } from '~/locale';
import MergeRequest from '../../../merge_request';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
import SquashBeforeMerge from './squash_before_merge.vue';
+import CommitsHeader from './commits_header.vue';
+import CommitEdit from './commit_edit.vue';
+import CommitMessageDropdown from './commit_message_dropdown.vue';
export default {
name: 'ReadyToMerge',
components: {
statusIcon,
SquashBeforeMerge,
+ CommitsHeader,
+ CommitEdit,
+ CommitMessageDropdown,
},
props: {
mr: { type: Object, required: true },
@@ -22,27 +29,20 @@ export default {
return {
removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false,
- useCommitMessageWithDescription: false,
setToMergeWhenPipelineSucceeds: false,
- showCommitMessageEditor: false,
isMakingRequest: false,
isMergingImmediately: false,
commitMessage: this.mr.commitMessage,
squashBeforeMerge: this.mr.squash,
successSvg,
warningSvg,
+ squashCommitMessage: this.mr.squashCommitMessage,
};
},
computed: {
shouldShowMergeWhenPipelineSucceedsText() {
return this.mr.isPipelineActive;
},
- commitMessageLinkTitle() {
- const withDesc = 'Include description in commit message';
- const withoutDesc = "Don't include description in commit message";
-
- return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
- },
status() {
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
@@ -84,9 +84,9 @@ export default {
},
mergeButtonText() {
if (this.isMergingImmediately) {
- return 'Merge in progress';
+ return __('Merge in progress');
} else if (this.shouldShowMergeWhenPipelineSucceedsText) {
- return 'Merge when pipeline succeeds';
+ return __('Merge when pipeline succeeds');
}
return 'Merge';
@@ -98,7 +98,7 @@ export default {
const { commitMessage } = this;
return Boolean(
!commitMessage.length ||
- !this.shouldShowMergeControls() ||
+ !this.shouldShowMergeControls ||
this.isMakingRequest ||
this.mr.preventMerge,
);
@@ -110,18 +110,14 @@ export default {
const { commitsCount, enableSquashBeforeMerge } = this.mr;
return enableSquashBeforeMerge && commitsCount > 1;
},
- },
- methods: {
shouldShowMergeControls() {
return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
},
- updateCommitMessage() {
- const cmwd = this.mr.commitMessageWithDescription;
- this.useCommitMessageWithDescription = !this.useCommitMessageWithDescription;
- this.commitMessage = this.useCommitMessageWithDescription ? cmwd : this.mr.commitMessage;
- },
- toggleCommitMessageEditor() {
- this.showCommitMessageEditor = !this.showCommitMessageEditor;
+ },
+ methods: {
+ updateMergeCommitMessage(includeDescription) {
+ const { commitMessageWithDescription, commitMessage } = this.mr;
+ this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
},
handleMergeButtonClick(mergeWhenBuildSucceeds, mergeImmediately) {
// TODO: Remove no-param-reassign
@@ -139,6 +135,7 @@ export default {
merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds,
should_remove_source_branch: this.removeSourceBranch === true,
squash: this.squashBeforeMerge,
+ squash_commit_message: this.squashCommitMessage,
};
this.isMakingRequest = true;
@@ -158,7 +155,7 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
- new Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ new Flash(__('Something went wrong. Please try again.')); // eslint-disable-line
});
},
initiateMergePolling() {
@@ -194,7 +191,7 @@ export default {
}
})
.catch(() => {
- new Flash('Something went wrong while merging this merge request. Please try again.'); // eslint-disable-line
+ new Flash(__('Something went wrong while merging this merge request. Please try again.')); // eslint-disable-line
});
},
initiateRemoveSourceBranchPolling() {
@@ -223,7 +220,7 @@ export default {
}
})
.catch(() => {
- new Flash('Something went wrong while deleting the source branch. Please try again.'); // eslint-disable-line
+ new Flash(__('Something went wrong while deleting the source branch. Please try again.')); // eslint-disable-line
});
},
},
@@ -231,127 +228,136 @@ export default {
</script>
<template>
- <div class="mr-widget-body media">
- <status-icon :status="iconClass" />
- <div class="media-body">
- <div class="mr-widget-body-controls media space-children">
- <span class="btn-group">
- <button
- :disabled="isMergeButtonDisabled"
- :class="mergeButtonClass"
- type="button"
- class="qa-merge-button"
- @click="handleMergeButtonClick()"
- >
- <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
- {{ mergeButtonText }}
- </button>
- <button
- v-if="shouldShowMergeOptionsDropdown"
- :disabled="isMergeButtonDisabled"
- type="button"
- class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
- data-toggle="dropdown"
- aria-label="Select merge moment"
- >
- <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i>
- </button>
- <ul
- v-if="shouldShowMergeOptionsDropdown"
- class="dropdown-menu dropdown-menu-right"
- role="menu"
- >
- <li>
- <a
- class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option"
- href="#"
- @click.prevent="handleMergeButtonClick(true)"
- >
- <span class="media">
- <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span>
- <span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
- </span>
- </a>
- </li>
- <li>
- <a
- class="accept-merge-request qa-merge-immediately-option"
- href="#"
- @click.prevent="handleMergeButtonClick(false, true)"
- >
- <span class="media">
- <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span>
- <span class="media-body merge-opt-title">Merge immediately</span>
- </span>
- </a>
- </li>
- </ul>
- </span>
- <div class="media-body-wrap space-children">
- <template v-if="shouldShowMergeControls()">
- <label v-if="mr.canRemoveSourceBranch">
- <input
- id="remove-source-branch-input"
- v-model="removeSourceBranch"
- :disabled="isRemoveSourceBranchButtonDisabled"
- class="js-remove-source-branch-checkbox"
- type="checkbox"
- />
- Delete source branch
- </label>
-
- <!-- Placeholder for EE extension of this component -->
- <squash-before-merge
- v-if="shouldShowSquashBeforeMerge"
- v-model="squashBeforeMerge"
- :help-path="mr.squashBeforeMergeHelpPath"
- :is-disabled="isMergeButtonDisabled"
- />
-
- <span v-if="mr.ffOnlyEnabled" class="js-fast-forward-message">
- Fast-forward merge without a merge commit
- </span>
+ <div>
+ <div class="mr-widget-body media">
+ <status-icon :status="iconClass" />
+ <div class="media-body">
+ <div class="mr-widget-body-controls media space-children">
+ <span class="btn-group">
<button
- v-else
:disabled="isMergeButtonDisabled"
- class="js-modify-commit-message-button btn btn-default btn-sm"
+ :class="mergeButtonClass"
type="button"
- @click="toggleCommitMessageEditor"
+ class="qa-merge-button"
+ @click="handleMergeButtonClick()"
>
- Modify commit message
+ <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ {{ mergeButtonText }}
</button>
- </template>
- <template v-else>
- <span class="bold js-resolve-mr-widget-items-message">
- You can only merge once the items above are resolved
- </span>
- </template>
- </div>
- </div>
- <div v-if="showCommitMessageEditor" class="prepend-top-default commit-message-editor">
- <div class="form-group clearfix">
- <label class="col-form-label" for="commit-message"> Commit message </label>
- <div class="col-sm-10">
- <div class="commit-message-container">
- <div class="max-width-marker"></div>
- <textarea
- id="commit-message"
- 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 href="#" @click.prevent="updateCommitMessage"> {{ commitMessageLinkTitle }} </a>
- </div>
+ <button
+ v-if="shouldShowMergeOptionsDropdown"
+ :disabled="isMergeButtonDisabled"
+ type="button"
+ class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
+ data-toggle="dropdown"
+ aria-label="Select merge moment"
+ >
+ <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i>
+ </button>
+ <ul
+ v-if="shouldShowMergeOptionsDropdown"
+ class="dropdown-menu dropdown-menu-right"
+ role="menu"
+ >
+ <li>
+ <a
+ class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option"
+ href="#"
+ @click.prevent="handleMergeButtonClick(true)"
+ >
+ <span class="media">
+ <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span>
+ <span class="media-body merge-opt-title">{{
+ __('Merge when pipeline succeeds')
+ }}</span>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ class="accept-merge-request qa-merge-immediately-option"
+ href="#"
+ @click.prevent="handleMergeButtonClick(false, true)"
+ >
+ <span class="media">
+ <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span>
+ <span class="media-body merge-opt-title">{{ __('Merge immediately') }}</span>
+ </span>
+ </a>
+ </li>
+ </ul>
+ </span>
+ <div class="media-body-wrap space-children">
+ <template v-if="shouldShowMergeControls">
+ <label v-if="mr.canRemoveSourceBranch">
+ <input
+ id="remove-source-branch-input"
+ v-model="removeSourceBranch"
+ :disabled="isRemoveSourceBranchButtonDisabled"
+ class="js-remove-source-branch-checkbox"
+ type="checkbox"
+ />
+ {{ __('Delete source branch') }}
+ </label>
+
+ <!-- Placeholder for EE extension of this component -->
+ <squash-before-merge
+ v-if="shouldShowSquashBeforeMerge"
+ v-model="squashBeforeMerge"
+ :help-path="mr.squashBeforeMergeHelpPath"
+ :is-disabled="isMergeButtonDisabled"
+ />
+ </template>
+ <template v-else>
+ <span class="bold js-resolve-mr-widget-items-message">
+ {{ __('You can only merge once the items above are resolved') }}
+ </span>
+ </template>
</div>
</div>
</div>
</div>
+ <template v-if="shouldShowMergeControls">
+ <div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message">
+ {{ __('Fast-forward merge without a merge commit') }}
+ </div>
+ <template v-else>
+ <commits-header
+ :is-squash-enabled="squashBeforeMerge"
+ :commits-count="mr.commitsCount"
+ :target-branch="mr.targetBranch"
+ >
+ <ul class="border-top content-list commits-list flex-list">
+ <commit-edit
+ v-if="squashBeforeMerge"
+ v-model="squashCommitMessage"
+ :label="__('Squash commit message')"
+ input-id="squash-message-edit"
+ squash
+ >
+ <commit-message-dropdown
+ slot="header"
+ v-model="squashCommitMessage"
+ :commits="mr.commits"
+ />
+ </commit-edit>
+ <commit-edit
+ v-model="commitMessage"
+ :label="__('Merge commit message')"
+ input-id="merge-message-edit"
+ >
+ <label slot="checkbox">
+ <input
+ id="include-description"
+ type="checkbox"
+ @change="updateMergeCommitMessage($event.target.checked)"
+ />
+ {{ __('Include merge request description') }}
+ </label>
+ </commit-edit>
+ </ul>
+ </commits-header>
+ </template>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js
deleted file mode 100644
index 8780aa4bd1c..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MRWidgetOptions from './mr_widget_options.vue';
-
-export default MRWidgetOptions;
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index 60cebbfc2b2..0cedbdbdfef 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import MrWidgetOptions from './ee_switch_mr_widget_options';
+import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
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 5a9d86594b1..abbbe19c5ef 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
@@ -3,6 +3,9 @@ import _ from 'underscore';
import { __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
+import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
+import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
+import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import createFlash from '../flash';
import WidgetHeader from './components/mr_widget_header.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
@@ -28,10 +31,7 @@ import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue';
import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue';
import CheckingState from './components/states/mr_widget_checking.vue';
-import MRWidgetStore from './stores/ee_switch_mr_widget_store';
-import MRWidgetService from './services/ee_switch_mr_widget_service';
import eventHub from './event_hub';
-import stateMaps from './stores/ee_switch_state_maps';
import notify from '~/lib/utils/notify';
import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
@@ -142,8 +142,8 @@ export default {
}
},
methods: {
- createService(store) {
- const endpoints = {
+ getServiceEndpoints(store) {
+ return {
mergePath: store.mergePath,
mergeCheckPath: store.mergeCheckPath,
cancelAutoMergePath: store.cancelAutoMergePath,
@@ -154,7 +154,9 @@ export default {
mergeActionsContentPath: store.mergeActionsContentPath,
rebasePath: store.rebasePath,
};
- return new MRWidgetService(endpoints);
+ },
+ createService(store) {
+ return new MRWidgetService(this.getServiceEndpoints(store));
},
checkStatus(cb, isRebased) {
return this.service
@@ -313,7 +315,7 @@ export default {
:endpoint="mr.testResultsPath"
/>
- <div class="mr-widget-section">
+ <div class="mr-widget-section p-0">
<component :is="componentName" :mr="mr" :service="service" />
<section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links">
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js
deleted file mode 100644
index ea2aabb78fe..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MRWidgetService from './mr_widget_service';
-
-export default MRWidgetService;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js
deleted file mode 100644
index ebef30e3eab..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import getStateKey from './get_state_key';
-
-export default getStateKey;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js
deleted file mode 100644
index 92a07c53f2d..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MergeRequestStore from './mr_widget_store';
-
-export default MergeRequestStore;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js
deleted file mode 100644
index 50cf9503ea7..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import stateMaps from './state_maps';
-
-export default stateMaps;
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 e5a52c6a7f6..58363f632a9 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
@@ -1,5 +1,5 @@
import Timeago from 'timeago.js';
-import getStateKey from './ee_switch_get_state_key';
+import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
@@ -32,16 +32,18 @@ export default class MergeRequestStore {
this.sourceBranchProtected = data.source_branch_protected;
this.conflictsDocsPath = data.conflicts_docs_path;
this.mergeStatus = data.merge_status;
- this.commitMessage = data.merge_commit_message;
+ this.commitMessage = data.default_merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
this.mergeCommitSha = data.merge_commit_sha;
- this.commitMessageWithDescription = data.merge_commit_message_with_description;
+ this.commitMessageWithDescription = data.default_merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || [];
+ this.commits = data.commits_without_merge_commits || [];
+ this.squashCommitMessage = data.default_squash_commit_message;
this.initRebase(data);
if (data.issues_links) {
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
index 0b0cd7b75eb..b57455adaad 100644
--- a/app/assets/javascripts/ide/components/file_finder/index.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -1,45 +1,62 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import Mousetrap from 'mousetrap';
import VirtualList from 'vue-virtual-scroll-list';
import Item from './item.vue';
-import router from '../../ide_router';
-import {
- MAX_FILE_FINDER_RESULTS,
- FILE_FINDER_ROW_HEIGHT,
- FILE_FINDER_EMPTY_ROW_HEIGHT,
-} from '../../constants';
-import {
- UP_KEY_CODE,
- DOWN_KEY_CODE,
- ENTER_KEY_CODE,
- ESC_KEY_CODE,
-} from '../../../lib/utils/keycodes';
+import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+
+export const MAX_FILE_FINDER_RESULTS = 40;
+export const FILE_FINDER_ROW_HEIGHT = 55;
+export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
+
+const originalStopCallback = Mousetrap.stopCallback;
export default {
components: {
Item,
VirtualList,
},
+ props: {
+ files: {
+ type: Array,
+ required: true,
+ },
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ showDiffStats: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ clearSearchOnClose: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
data() {
return {
- focusedIndex: 0,
+ focusedIndex: -1,
searchText: '',
mouseOver: false,
cancelMouseOver: false,
};
},
computed: {
- ...mapGetters(['allBlobs']),
- ...mapState(['fileFindVisible', 'loading']),
filteredBlobs() {
const searchText = this.searchText.trim();
if (searchText === '') {
- return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
+ return this.files.slice(0, MAX_FILE_FINDER_RESULTS);
}
- return fuzzaldrinPlus.filter(this.allBlobs, searchText, {
+ return fuzzaldrinPlus.filter(this.files, searchText, {
key: 'path',
maxResults: MAX_FILE_FINDER_RESULTS,
});
@@ -58,10 +75,12 @@ export default {
},
},
watch: {
- fileFindVisible() {
+ visible() {
this.$nextTick(() => {
- if (!this.fileFindVisible) {
- this.searchText = '';
+ if (!this.visible) {
+ if (this.clearSearchOnClose) {
+ this.searchText = '';
+ }
} else {
this.focusedIndex = 0;
@@ -72,7 +91,11 @@ export default {
});
},
searchText() {
- this.focusedIndex = 0;
+ this.focusedIndex = -1;
+
+ this.$nextTick(() => {
+ this.focusedIndex = 0;
+ });
},
focusedIndex() {
if (!this.mouseOver) {
@@ -98,8 +121,25 @@ export default {
}
},
},
+ mounted() {
+ if (this.files.length) {
+ this.focusedIndex = 0;
+ }
+
+ Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+
+ this.toggle(!this.visible);
+ });
+
+ Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
+ },
methods: {
- ...mapActions(['toggleFileFinder']),
+ toggle(visible) {
+ this.$emit('toggle', visible);
+ },
clearSearchInput() {
this.searchText = '';
@@ -139,15 +179,15 @@ export default {
this.openFile(this.filteredBlobs[this.focusedIndex]);
break;
case ESC_KEY_CODE:
- this.toggleFileFinder(false);
+ this.toggle(false);
break;
default:
break;
}
},
openFile(file) {
- this.toggleFileFinder(false);
- router.push(`/project${file.url}`);
+ this.toggle(false);
+ this.$emit('click', file);
},
onMouseOver(index) {
if (!this.cancelMouseOver) {
@@ -159,14 +199,26 @@ export default {
this.cancelMouseOver = false;
this.onMouseOver(index);
},
+ mousetrapStopCallback(e, el, combo) {
+ if (
+ (combo === 't' && el.classList.contains('dropdown-input-field')) ||
+ el.classList.contains('inputarea')
+ ) {
+ return true;
+ } else if (combo === 'command+p' || combo === 'ctrl+p') {
+ return false;
+ }
+
+ return originalStopCallback(e, el, combo);
+ },
},
};
</script>
<template>
- <div class="ide-file-finder-overlay" @mousedown.self="toggleFileFinder(false)">
- <div class="dropdown-menu diff-file-changes ide-file-finder show">
- <div class="dropdown-input">
+ <div class="file-finder-overlay" @mousedown.self="toggle(false)">
+ <div class="dropdown-menu diff-file-changes file-finder show">
+ <div :class="{ 'has-value': showClearInputButton }" class="dropdown-input">
<input
ref="searchInput"
v-model="searchText"
@@ -186,9 +238,6 @@ export default {
></i>
<i
:aria-label="__('Clear search input')"
- :class="{
- show: showClearInputButton,
- }"
role="button"
class="fa fa-times dropdown-input-clear"
@click="clearSearchInput"
@@ -203,6 +252,7 @@ export default {
:search-text="searchText"
:focused="index === focusedIndex"
:index="index"
+ :show-diff-stats="showDiffStats"
class="disable-hover"
@click="openFile"
@mouseover="onMouseOver"
@@ -225,3 +275,25 @@ export default {
</div>
</div>
</template>
+
+<style scoped>
+.file-finder-overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 200;
+}
+
+.file-finder {
+ top: 10px;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.diff-file-changes {
+ top: 50px;
+ max-height: 327px;
+}
+</style>
diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
index 83e80d50aff..73511879ff2 100644
--- a/app/assets/javascripts/ide/components/file_finder/item.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
@@ -1,5 +1,6 @@
<script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '../../../vue_shared/components/file_icon.vue';
import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue';
@@ -7,6 +8,7 @@ const MAX_PATH_LENGTH = 60;
export default {
components: {
+ Icon,
ChangedFileIcon,
FileIcon,
},
@@ -27,6 +29,11 @@ export default {
type: Number,
required: true,
},
+ showDiffStats: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
pathWithEllipsis() {
@@ -97,8 +104,23 @@ export default {
</span>
</span>
</span>
- <span v-if="file.changed || file.tempFile" class="diff-changed-stats">
- <changed-file-icon :file="file" />
+ <span v-if="file.changed || file.tempFile" v-once class="diff-changed-stats">
+ <span v-if="showDiffStats">
+ <span class="cgreen bold">
+ <icon name="file-addition" class="align-text-top" /> {{ file.addedLines }}
+ </span>
+ <span class="cred bold ml-1">
+ <icon name="file-deletion" class="align-text-top" /> {{ file.removedLines }}
+ </span>
+ </span>
+ <changed-file-icon v-else :file="file" />
</span>
</button>
</template>
+
+<style scoped>
+.highlighted {
+ color: #1f78d1;
+ font-weight: 600;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index cc07ef46064..3f607aa2a0a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -27,11 +27,6 @@ export default {
type: String,
required: true,
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
addSpacingClasses: {
type: Boolean,
required: false,
@@ -158,7 +153,7 @@ export default {
this.markdownPreviewLoading = true;
this.markdownPreview = __('Loading…');
this.$http
- .post(this.versionedPreviewPath(), { text })
+ .post(this.markdownPreviewPath, { text })
.then(resp => resp.json())
.then(data => this.renderMarkdown(data))
.catch(() => new Flash(__('Error loading markdown preview')));
@@ -186,13 +181,6 @@ export default {
.then(() => $(this.$refs['markdown-preview']).renderGFM())
.catch(() => new Flash(__('Error rendering markdown preview')));
},
-
- versionedPreviewPath() {
- const { markdownPreviewPath, markdownVersion } = this;
- return `${markdownPreviewPath}${
- markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
- }markdown_version=${markdownVersion}`;
- },
},
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
index 8bdb5bf22c2..13eb46437dd 100644
--- a/app/assets/javascripts/vue_shared/components/pikaday.vue
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -40,6 +40,7 @@ export default {
toString: date => pikadayToString(date),
onSelect: this.selected.bind(this),
onClose: this.toggled.bind(this),
+ firstDay: gon.first_day_of_week,
});
this.$el.append(this.calendar.el);
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index d24fe1b547e..f9773622001 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -28,10 +28,10 @@ export default {
},
computed: {
statusHtml() {
- if (this.user.status.emoji && this.user.status.message) {
- return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message}`;
- } else if (this.user.status.message) {
- return this.user.status.message;
+ if (this.user.status.emoji && this.user.status.message_html) {
+ return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`;
+ } else if (this.user.status.message_html) {
+ return this.user.status.message_html;
}
return '';
},
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 0fb9bde1785..c5c3b66438c 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -89,6 +89,10 @@ hr {
.str-truncated {
@include str-truncated;
+ &-30 {
+ @include str-truncated(30%);
+ }
+
&-60 {
@include str-truncated(60%);
}
@@ -387,14 +391,26 @@ img.emoji {
.flex-no-shrink { flex-shrink: 0; }
.ws-initial { white-space: initial; }
.overflow-auto { overflow: auto; }
+.d-flex-center {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
/** COMMON SIZING CLASSES **/
.w-0 { width: 0; }
-.h-13em { height: 13em; }
+
+.h-12em { height: 12em; }
+
.mw-460 { max-width: 460px; }
.mw-6em { max-width: 6em; }
+
.min-height-0 { min-height: 0; }
+.w-3 { width: #{3 * $grid-size}; }
+
+.h-3 { width: #{3 * $grid-size}; }
+
/** COMMON SPACING CLASSES **/
.gl-pl-0 { padding-left: 0; }
.gl-pl-1 { padding-left: #{0.5 * $grid-size}; }
@@ -420,3 +436,9 @@ img.emoji {
.ms-no-clear ::-ms-clear {
display: none;
}
+
+/** COMMON POSITIONING CLASSES */
+.position-bottom-0 { bottom: 0; }
+.position-left-0 { left: 0; }
+.position-right-0 { right: 0; }
+.position-top-0 { top: 0; }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 037a5adfb7e..6108eaa1ad0 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -4,6 +4,7 @@
*/
.file-holder {
border: 1px solid $border-color;
+ border-top: 0;
border-radius: $border-radius-default;
&.file-holder-no-border {
@@ -51,6 +52,7 @@
position: absolute;
top: 5px;
right: 15px;
+ margin-left: auto;
.btn {
padding: 0 10px;
@@ -324,10 +326,12 @@ span.idiff {
&,
.file-holder & {
display: flex;
+ flex-wrap: wrap;
align-items: center;
justify-content: space-between;
background-color: $gray-light;
border-bottom: 1px solid $border-color;
+ border-top: 1px solid $border-color;
padding: 5px $gl-padding;
margin: 0;
border-radius: $border-radius-default $border-radius-default 0 0;
@@ -365,16 +369,12 @@ span.idiff {
margin: 0 10px 0 0;
}
- .file-actions {
- white-space: nowrap;
-
- .btn {
- padding: 0 10px;
- font-size: 13px;
- line-height: 28px;
- display: inline-block;
- float: none;
- }
+ .file-actions .btn {
+ padding: 0 10px;
+ font-size: 13px;
+ line-height: 28px;
+ display: inline-block;
+ float: none;
}
@include media-breakpoint-down(xs) {
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index a20920e2503..d78c707192f 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -38,7 +38,10 @@
svg {
fill: currentColor;
+}
+.square,
+svg {
$svg-sizes: 8 10 12 14 16 18 24 32 48 72;
@each $svg-size in $svg-sizes {
&.s#{$svg-size} {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 9eae9a831fa..96dab609a13 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -243,6 +243,7 @@ $gl-padding-8: 8px;
$gl-padding: 16px;
$gl-padding-24: 24px;
$gl-padding-32: 32px;
+$gl-padding-50: 50px;
$gl-col-padding: 15px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px;
@@ -490,6 +491,7 @@ $builds-trace-bg: #111;
*/
$commit-max-width-marker-color: rgba(0, 0, 0, 0);
$commit-message-text-area-bg: rgba(0, 0, 0, 0);
+$commit-stat-summary-height: 36px;
/*
* Common
@@ -664,8 +666,14 @@ $priority-label-empty-state-width: 114px;
Issues Analytics
*/
$issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
+
/*
Merge Requests
*/
$mr-tabs-height: 51px;
$mr-version-controls-height: 56px;
+
+/*
+Compare Branches
+*/
+$compare-branches-sticky-header-height: 68px;
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 553cc44fe83..2ac98b5d18f 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -395,6 +395,11 @@ $ide-commit-header-height: 48px;
svg {
vertical-align: sub;
}
+
+ .ide-status-avatar {
+ float: none;
+ margin: 0 0 1px;
+ }
}
.ide-status-file {
@@ -811,26 +816,6 @@ $ide-commit-header-height: 48px;
z-index: 1;
}
-.ide-file-finder-overlay {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- z-index: 100;
-}
-
-.ide-file-finder {
- top: 10px;
- left: 50%;
- transform: translateX(-50%);
-
- .highlighted {
- color: $blue-500;
- font-weight: $gl-font-weight-bold;
- }
-}
-
.ide-commit-message-field {
height: 200px;
background-color: $white-light;
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index ad12cd101b6..809ba6d4953 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -58,6 +58,20 @@
}
}
+.cluster-application-banner {
+ height: 45px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.cluster-application-banner-close {
+ align-self: flex-start;
+ font-weight: 500;
+ font-size: 20px;
+ margin: $gl-padding-8 14px 0 0;
+}
+
.cluster-application-description {
flex: 1;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 02aac58a475..e3b98b26a11 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -7,22 +7,13 @@
cursor: pointer;
@media (min-width: map-get($grid-breakpoints, md)) {
+ $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height;
+
position: -webkit-sticky;
position: sticky;
- top: $mr-version-controls-height + $header-height + $mr-tabs-height;
- margin-left: -1px;
- border-left: 1px solid $border-color;
+ top: $mr-file-header-top;
z-index: 102;
- &.is-commit {
- top: $header-height + 36px;
-
- .with-performance-bar & {
- top: $header-height + 36px + $performance-bar-height;
-
- }
- }
-
&::before {
content: '';
position: absolute;
@@ -35,7 +26,23 @@
}
.with-performance-bar & {
- top: $header-height + $performance-bar-height + $mr-version-controls-height + $mr-tabs-height;
+ top: $mr-file-header-top + $performance-bar-height;
+ }
+
+ &.is-commit {
+ top: $header-height + $commit-stat-summary-height;
+
+ .with-performance-bar & {
+ top: $header-height + $commit-stat-summary-height + $performance-bar-height;
+ }
+ }
+
+ &.is-compare {
+ top: $header-height + $compare-branches-sticky-header-height;
+
+ .with-performance-bar & {
+ top: $performance-bar-height + $header-height + $compare-branches-sticky-header-height;
+ }
}
}
@@ -501,6 +508,25 @@
}
}
+.diff-stats {
+ align-items: center;
+ padding: 0 .25rem;
+
+ .diff-stats-group {
+ padding: 0 .25rem;
+ }
+
+ svg.diff-stats-icon {
+ vertical-align: text-bottom;
+ }
+
+ &.is-compare-versions-header {
+ .diff-stats-group {
+ padding: 0 .5rem;
+ }
+ }
+}
+
.file-content .diff-file {
margin: 0;
border: 0;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index b6abb792709..61ecf133b02 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -240,18 +240,7 @@
}
.prometheus-graph {
- flex: 1 0 auto;
- min-width: 450px;
- max-width: 100%;
padding: $gl-padding / 2;
-
- h5 {
- font-size: 16px;
- }
-
- @include media-breakpoint-down(sm) {
- min-width: 100%;
- }
}
.prometheus-graph-header {
@@ -261,6 +250,7 @@
margin-bottom: $gl-padding-8;
h5 {
+ font-size: $gl-font-size-large;
margin: 0;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 53afb182b54..135730d71e9 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -38,9 +38,7 @@
}
.mr-widget-section {
- .media {
- align-items: center;
- }
+ border-radius: $border-radius-default $border-radius-default 0 0;
.code-text {
flex: 1;
@@ -56,6 +54,11 @@
.mr-widget-extension {
border-top: 1px solid $border-color;
background-color: $gray-light;
+
+ &.clickable:hover {
+ background-color: $gl-gray-200;
+ cursor: pointer;
+ }
}
.mr-widget-workflow {
@@ -78,6 +81,7 @@
border-top: 0;
}
+.mr-widget-body,
.mr-widget-section,
.mr-widget-content,
.mr-widget-footer {
@@ -87,11 +91,38 @@
.mr-state-widget {
color: $gl-text-color;
+ .commit-message-edit {
+ border-radius: $border-radius-default;
+ }
+
.mr-widget-section,
.mr-widget-footer {
border-top: solid 1px $border-color;
}
+ .mr-fast-forward-message {
+ padding-left: $gl-padding-50;
+ padding-bottom: $gl-padding;
+ }
+
+ .commits-list {
+ > li {
+ padding: $gl-padding;
+
+ @include media-breakpoint-up(md) {
+ padding-left: $gl-padding-50;
+ }
+ }
+ }
+
+ .mr-commit-dropdown {
+ .dropdown-menu {
+ @include media-breakpoint-up(md) {
+ width: 150%;
+ }
+ }
+ }
+
.mr-widget-footer {
padding: 0;
}
@@ -405,7 +436,7 @@
}
.mr-widget-help {
- padding: 10px 16px 10px 48px;
+ padding: 10px 16px 10px $gl-padding-50;
font-style: italic;
}
@@ -423,10 +454,6 @@
}
}
-.mr-widget-body-controls {
- flex-wrap: wrap;
-}
-
.mr_source_commit,
.mr_target_commit {
margin-bottom: 0;
@@ -986,3 +1013,9 @@
width: $ci-action-icon-size-lg;
}
}
+
+.merge-request-details .file-finder-overlay.diff-file-finder {
+ position: fixed;
+ z-index: 99999;
+ background: $black-transparent;
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 2342c284a5e..66866aedfba 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -507,12 +507,6 @@
}
.template-option {
- .logo {
- .btn-template-icon {
- width: 40px !important;
- }
- }
-
padding: 16px 0;
&:not(:first-child) {
@@ -551,9 +545,8 @@
}
.selected-icon {
- svg {
+ img {
display: none;
- top: 7px;
height: 20px;
width: 20px;
}
@@ -946,6 +939,11 @@ pre.light-well {
.flex-wrapper {
min-width: 0;
margin-top: -$gl-padding-8; // negative margin required for flex-wrap
+ flex: 1 1 100%;
+
+ .project-title {
+ line-height: 20px;
+ }
}
p,
@@ -984,14 +982,16 @@ pre.light-well {
}
.controls {
- margin-top: $gl-padding-8;
+ @include media-breakpoint-down(xs) {
+ margin-top: $gl-padding-8;
+ }
- @include media-breakpoint-down(md) {
+ @include media-breakpoint-up(sm) {
margin-top: 0;
}
- @include media-breakpoint-down(xs) {
- margin-top: $gl-padding-8;
+ @include media-breakpoint-up(lg) {
+ flex: 1 1 40%;
}
.icon-wrapper {
@@ -1041,7 +1041,7 @@ pre.light-well {
min-height: 40px;
min-width: 40px;
- .identicon.s64 {
+ .identicon.s48 {
font-size: 16px;
}
}
diff --git a/app/assets/stylesheets/pages/serverless.scss b/app/assets/stylesheets/pages/serverless.scss
new file mode 100644
index 00000000000..a5b73492380
--- /dev/null
+++ b/app/assets/stylesheets/pages/serverless.scss
@@ -0,0 +1,3 @@
+.url-text-field {
+ cursor: text;
+}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index c5b9d1f6885..811cc310a8f 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -167,12 +167,14 @@
font-weight: $gl-font-weight-normal;
display: inline-block;
color: $gl-text-color;
+ vertical-align: top;
}
.option-description,
.option-disabled-reason {
margin-left: 30px;
color: $project-option-descr-color;
+ margin-top: -5px;
}
.option-disabled-reason {