Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-11 15:08:52 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-11 15:08:52 +0300
commit9f5ac379c76c278ee9ee1662e26c4612b0a117bd (patch)
tree49cd59544c083678fefd1e77340ca5e2b6e3565c /app
parent7240fb1a06c9e1b254719426b1ac96ec2f00fe35 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/actioncable_connection_monitor.js142
-rw-r--r--app/assets/javascripts/actioncable_consumer.js9
-rw-r--r--app/assets/javascripts/admin/users/components/user_avatar.vue17
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_trigger.vue4
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue15
-rw-r--r--app/assets/javascripts/boards/components/toggle_focus.vue22
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_job_details_container.vue7
-rw-r--r--app/assets/javascripts/label_manager.js9
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js34
-rw-r--r--app/assets/javascripts/pages/projects/environments/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js18
-rw-r--r--app/assets/javascripts/pages/projects/tags/show/index.js10
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue2
-rw-r--r--app/controllers/projects/notes_controller.rb7
-rw-r--r--app/controllers/projects/project_members_controller.rb2
-rw-r--r--app/graphql/mutations/notes/create/base.rb10
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/models/application_setting.rb4
-rw-r--r--app/models/application_setting_implementation.rb11
-rw-r--r--app/models/design_management/design.rb11
-rw-r--r--app/services/design_management/move_designs_service.rb7
-rw-r--r--app/services/merge_requests/update_service.rb24
-rw-r--r--app/views/admin/application_settings/_note_limits.html.haml3
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/devise/shared/_signup_omniauth_provider_list.haml2
-rw-r--r--app/views/groups/_new_group_fields.html.haml4
-rw-r--r--app/views/profiles/_email_settings.html.haml2
-rw-r--r--app/views/profiles/_name.html.haml4
-rw-r--r--app/views/profiles/show.html.haml22
-rw-r--r--app/views/projects/_project_templates.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/shared/_label.html.haml2
-rw-r--r--app/views/shared/issuable/_board_create_list_dropdown.html.haml4
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml6
-rw-r--r--app/views/shared/issuable/_sidebar_todo.html.haml2
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml4
38 files changed, 304 insertions, 129 deletions
diff --git a/app/assets/javascripts/actioncable_connection_monitor.js b/app/assets/javascripts/actioncable_connection_monitor.js
new file mode 100644
index 00000000000..fc4e436c7fb
--- /dev/null
+++ b/app/assets/javascripts/actioncable_connection_monitor.js
@@ -0,0 +1,142 @@
+/* eslint-disable no-restricted-globals */
+
+import { logger } from '@rails/actioncable';
+
+// This is based on https://github.com/rails/rails/blob/5a477890c809d4a17dc0dede43c6b8cef81d8175/actioncable/app/javascript/action_cable/connection_monitor.js
+// so that we can take advantage of the improved reconnection logic. We can remove this once we upgrade @rails/actioncable to a version that includes this.
+
+// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting
+// revival reconnections if things go astray. Internal class, not intended for direct user manipulation.
+
+const now = () => new Date().getTime();
+
+const secondsSince = (time) => (now() - time) / 1000;
+class ConnectionMonitor {
+ constructor(connection) {
+ this.visibilityDidChange = this.visibilityDidChange.bind(this);
+ this.connection = connection;
+ this.reconnectAttempts = 0;
+ }
+
+ start() {
+ if (!this.isRunning()) {
+ this.startedAt = now();
+ delete this.stoppedAt;
+ this.startPolling();
+ addEventListener('visibilitychange', this.visibilityDidChange);
+ logger.log(
+ `ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`,
+ );
+ }
+ }
+
+ stop() {
+ if (this.isRunning()) {
+ this.stoppedAt = now();
+ this.stopPolling();
+ removeEventListener('visibilitychange', this.visibilityDidChange);
+ logger.log('ConnectionMonitor stopped');
+ }
+ }
+
+ isRunning() {
+ return this.startedAt && !this.stoppedAt;
+ }
+
+ recordPing() {
+ this.pingedAt = now();
+ }
+
+ recordConnect() {
+ this.reconnectAttempts = 0;
+ this.recordPing();
+ delete this.disconnectedAt;
+ logger.log('ConnectionMonitor recorded connect');
+ }
+
+ recordDisconnect() {
+ this.disconnectedAt = now();
+ logger.log('ConnectionMonitor recorded disconnect');
+ }
+
+ // Private
+
+ startPolling() {
+ this.stopPolling();
+ this.poll();
+ }
+
+ stopPolling() {
+ clearTimeout(this.pollTimeout);
+ }
+
+ poll() {
+ this.pollTimeout = setTimeout(() => {
+ this.reconnectIfStale();
+ this.poll();
+ }, this.getPollInterval());
+ }
+
+ getPollInterval() {
+ const { staleThreshold, reconnectionBackoffRate } = this.constructor;
+ const backoff = (1 + reconnectionBackoffRate) ** Math.min(this.reconnectAttempts, 10);
+ const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate;
+ const jitter = jitterMax * Math.random();
+ return staleThreshold * 1000 * backoff * (1 + jitter);
+ }
+
+ reconnectIfStale() {
+ if (this.connectionIsStale()) {
+ logger.log(
+ `ConnectionMonitor detected stale connection. reconnectAttempts = ${
+ this.reconnectAttempts
+ }, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${
+ this.constructor.staleThreshold
+ } s`,
+ );
+ this.reconnectAttempts += 1;
+ if (this.disconnectedRecently()) {
+ logger.log(
+ `ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(
+ this.disconnectedAt,
+ )} s`,
+ );
+ } else {
+ logger.log('ConnectionMonitor reopening');
+ this.connection.reopen();
+ }
+ }
+ }
+
+ get refreshedAt() {
+ return this.pingedAt ? this.pingedAt : this.startedAt;
+ }
+
+ connectionIsStale() {
+ return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
+ }
+
+ disconnectedRecently() {
+ return (
+ this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold
+ );
+ }
+
+ visibilityDidChange() {
+ if (document.visibilityState === 'visible') {
+ setTimeout(() => {
+ if (this.connectionIsStale() || !this.connection.isOpen()) {
+ logger.log(
+ `ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`,
+ );
+ this.connection.reopen();
+ }
+ }, 200);
+ }
+ }
+}
+
+ConnectionMonitor.staleThreshold = 6; // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
+ConnectionMonitor.reconnectionBackoffRate = 0.15;
+
+export default ConnectionMonitor;
diff --git a/app/assets/javascripts/actioncable_consumer.js b/app/assets/javascripts/actioncable_consumer.js
index 5658ffc1a38..aeb61e61a3d 100644
--- a/app/assets/javascripts/actioncable_consumer.js
+++ b/app/assets/javascripts/actioncable_consumer.js
@@ -1,3 +1,10 @@
import { createConsumer } from '@rails/actioncable';
+import ConnectionMonitor from './actioncable_connection_monitor';
-export default createConsumer();
+const consumer = createConsumer();
+
+if (consumer.connection) {
+ consumer.connection.monitor = new ConnectionMonitor(consumer.connection);
+}
+
+export default consumer;
diff --git a/app/assets/javascripts/admin/users/components/user_avatar.vue b/app/assets/javascripts/admin/users/components/user_avatar.vue
index ff0e91fcb8f..ce22595609d 100644
--- a/app/assets/javascripts/admin/users/components/user_avatar.vue
+++ b/app/assets/javascripts/admin/users/components/user_avatar.vue
@@ -1,5 +1,5 @@
<script>
-import { GlAvatarLink, GlAvatarLabeled, GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlAvatarLabeled, GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { truncate } from '~/lib/utils/text_utility';
import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from '../constants';
@@ -8,7 +8,6 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- GlAvatarLink,
GlAvatarLabeled,
GlBadge,
GlIcon,
@@ -27,6 +26,11 @@ export default {
adminUserHref() {
return this.adminUserPath.replace('id', this.user.username);
},
+ adminUserMailto() {
+ // NOTE: 'mailto:' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `mailto:${this.user.email}`;
+ },
userNoteShort() {
return truncate(this.user.note, LENGTH_OF_USER_NOTE_TOOLTIP);
},
@@ -36,10 +40,9 @@ export default {
</script>
<template>
- <gl-avatar-link
+ <div
v-if="user"
- class="js-user-link"
- :href="adminUserHref"
+ class="js-user-link gl-display-inline-block"
:data-user-id="user.id"
:data-username="user.username"
>
@@ -48,6 +51,8 @@ export default {
:src="user.avatarUrl"
:label="user.name"
:sub-label="user.email"
+ :label-link="adminUserHref"
+ :sub-label-link="adminUserMailto"
>
<template #meta>
<div v-if="user.note" class="gl-text-gray-500 gl-p-1">
@@ -60,5 +65,5 @@ export default {
</div>
</template>
</gl-avatar-labeled>
- </gl-avatar-link>
+ </div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
index ea68df9ce12..85fca589279 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
@@ -13,8 +13,8 @@ export default {
</script>
<template>
- <span class="gl-ml-4">
- <gl-button variant="success" @click="setAddColumnFormVisibility(true)"
+ <span class="gl-ml-3 gl-display-flex gl-align-items-center">
+ <gl-button variant="confirm" @click="setAddColumnFormVisibility(true)"
>{{ __('Create list') }}
</gl-button>
</span>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
index a2dbd52369f..64d02dbdc54 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
@@ -10,7 +10,6 @@ import {
} from '@gitlab/ui';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import createFlash from '~/flash';
-import { BV_DROPDOWN_HIDE } from '~/lib/utils/constants';
import { __, s__ } from '~/locale';
import projectMilestones from '../../graphql/project_milestones.query.graphql';
@@ -73,21 +72,20 @@ export default {
return this.activeIssue.milestone?.title ?? this.$options.i18n.noMilestone;
},
},
- mounted() {
- this.$root.$on(BV_DROPDOWN_HIDE, () => {
- this.$refs.sidebarItem.collapse();
- });
- },
methods: {
...mapActions(['setActiveIssueMilestone']),
handleOpen() {
this.edit = true;
this.$refs.dropdown.show();
},
+ handleClose() {
+ this.edit = false;
+ this.$refs.sidebarItem.collapse();
+ },
async setMilestone(milestoneId) {
this.loading = true;
this.searchTitle = '';
- this.$refs.sidebarItem.collapse();
+ this.handleClose();
try {
const input = { milestoneId, projectPath: this.projectPath };
@@ -116,7 +114,7 @@ export default {
:title="$options.i18n.milestone"
:loading="loading"
@open="handleOpen()"
- @close="edit = false"
+ @close="handleClose"
>
<template v-if="hasMilestone" #collapsed>
<strong class="gl-text-gray-900">{{ activeIssue.milestone.title }}</strong>
@@ -126,6 +124,7 @@ export default {
:text="dropdownText"
:header-text="$options.i18n.assignMilestone"
block
+ @hide="handleClose"
>
<gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" />
<gl-dropdown-item
diff --git a/app/assets/javascripts/boards/components/toggle_focus.vue b/app/assets/javascripts/boards/components/toggle_focus.vue
index 59ee47937c9..74805f8a681 100644
--- a/app/assets/javascripts/boards/components/toggle_focus.vue
+++ b/app/assets/javascripts/boards/components/toggle_focus.vue
@@ -1,11 +1,14 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import { __ } from '~/locale';
import { hide } from '~/tooltips';
export default {
components: {
- GlIcon,
+ GlButton,
+ },
+ directives: {
+ GlTooltip,
},
props: {
issueBoardsContentSelector: {
@@ -35,18 +38,15 @@ export default {
</script>
<template>
- <div class="board-extra-actions">
- <a
+ <div class="board-extra-actions gl-ml-3 gl-display-flex gl-align-items-center">
+ <gl-button
ref="toggleFocusModeButton"
- href="#"
- class="btn btn-default has-tooltip gl-ml-3 js-focus-mode-btn"
+ v-gl-tooltip
+ :icon="isFullscreen ? 'minimize' : 'maximize'"
+ class="js-focus-mode-btn"
data-qa-selector="focus_mode_button"
- role="button"
- :aria-label="$options.i18n.toggleFocusMode"
:title="$options.i18n.toggleFocusMode"
@click="toggleFocusMode"
- >
- <gl-icon :name="isFullscreen ? 'minimize' : 'maximize'" />
- </a>
+ />
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
index 84ce6674104..84883ead125 100644
--- a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
@@ -95,7 +95,12 @@ export default {
<p v-if="hasTags" class="build-detail-row" data-testid="job-tags">
<span class="font-weight-bold">{{ __('Tags:') }}</span>
- <span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary mr-1">{{ tag }}</span>
+ <span
+ v-for="(tag, i) in job.tags"
+ :key="i"
+ class="badge badge-pill badge-primary gl-badge sm"
+ >{{ tag }}</span
+ >
</p>
</div>
</template>
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index aa197edd449..2a020a66fd2 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -2,8 +2,7 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
-
-import { hide, dispose } from '~/tooltips';
+import { dispose } from '~/tooltips';
import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
@@ -30,7 +29,6 @@ export default class LabelManager {
}
bindEvents() {
- this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick);
return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
}
@@ -46,11 +44,6 @@ export default class LabelManager {
_this.toggleEmptyState($label, $btn, action);
}
- onButtonActionClick(e) {
- e.stopPropagation();
- hide(e.currentTarget);
- }
-
toggleEmptyState() {
this.emptyState.classList.toggle(
'hidden',
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 413e43c638b..16e7645592c 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -12,25 +12,23 @@ import initSearchSettings from '~/search_settings';
import initProjectPermissionsSettings from '../shared/permissions';
import initProjectLoadingSpinner from '../shared/save_project_loader';
-document.addEventListener('DOMContentLoaded', () => {
- initFilePickers();
- initConfirmDangerModal();
- initSettingsPanels();
- initProjectDeleteButton();
- mountBadgeSettings(PROJECT_BADGE);
+initFilePickers();
+initConfirmDangerModal();
+initSettingsPanels();
+initProjectDeleteButton();
+mountBadgeSettings(PROJECT_BADGE);
- new UserCallout({ className: 'js-service-desk-callout' }); // eslint-disable-line no-new
- initServiceDesk();
+new UserCallout({ className: 'js-service-desk-callout' }); // eslint-disable-line no-new
+initServiceDesk();
- initProjectLoadingSpinner();
- initProjectPermissionsSettings();
- setupTransferEdit('.js-project-transfer-form', 'select.select2');
+initProjectLoadingSpinner();
+initProjectPermissionsSettings();
+setupTransferEdit('.js-project-transfer-form', 'select.select2');
- dirtySubmitFactory(
- document.querySelectorAll(
- '.js-general-settings-form, .js-mr-settings-form, .js-mr-approvals-form',
- ),
- );
+dirtySubmitFactory(
+ document.querySelectorAll(
+ '.js-general-settings-form, .js-mr-settings-form, .js-mr-approvals-form',
+ ),
+);
- initSearchSettings();
-});
+initSearchSettings();
diff --git a/app/assets/javascripts/pages/projects/environments/index/index.js b/app/assets/javascripts/pages/projects/environments/index/index.js
index 4d5106f6d5f..554ed4f9786 100644
--- a/app/assets/javascripts/pages/projects/environments/index/index.js
+++ b/app/assets/javascripts/pages/projects/environments/index/index.js
@@ -1,3 +1,3 @@
import initEnvironments from '~/environments/';
-document.addEventListener('DOMContentLoaded', initEnvironments);
+initEnvironments();
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index f029b26fa78..2730e0f0b84 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -23,17 +23,15 @@ function mountRemoveMemberModal() {
});
}
-document.addEventListener('DOMContentLoaded', () => {
- groupsSelect();
- memberExpirationDate();
- memberExpirationDate('.js-access-expiration-date-groups');
- mountRemoveMemberModal();
- initInviteMembersModal();
- initInviteMembersTrigger();
+groupsSelect();
+memberExpirationDate();
+memberExpirationDate('.js-access-expiration-date-groups');
+mountRemoveMemberModal();
+initInviteMembersModal();
+initInviteMembersTrigger();
- new Members(); // eslint-disable-line no-new
- new UsersSelect(); // eslint-disable-line no-new
-});
+new Members(); // eslint-disable-line no-new
+new UsersSelect(); // eslint-disable-line no-new
if (window.gon.features.vueProjectMembersList) {
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
diff --git a/app/assets/javascripts/pages/projects/tags/show/index.js b/app/assets/javascripts/pages/projects/tags/show/index.js
index 651cc05ca4f..6f5406f554f 100644
--- a/app/assets/javascripts/pages/projects/tags/show/index.js
+++ b/app/assets/javascripts/pages/projects/tags/show/index.js
@@ -1,10 +1,8 @@
import { redirectTo, getBaseURL, stripFinalUrlSegment } from '~/lib/utils/url_utility';
import { initRemoveTag } from '../remove_tag';
-document.addEventListener('DOMContentLoaded', () => {
- initRemoveTag({
- onDelete: (path = '') => {
- redirectTo(stripFinalUrlSegment([getBaseURL(), path].join('')));
- },
- });
+initRemoveTag({
+ onDelete: (path = '') => {
+ redirectTo(stripFinalUrlSegment([getBaseURL(), path].join('')));
+ },
});
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index 4a4cbbdaa70..de4bbb36141 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -109,7 +109,7 @@ export default {
<div
v-for="(key, keyIndex) in keys"
:key="key"
- class="break-word gl-text-black-normal"
+ class="break-word"
:class="{ 'mb-3 bold': keyIndex == 0 }"
>
{{ item[key] }}
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 0b1d7d24d21..71a93701dc4 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -94,8 +94,7 @@ class Projects::NotesController < Projects::ApplicationController
def create_rate_limit
key = :notes_create
-
- return unless rate_limiter.throttled?(key, scope: [current_user])
+ return unless rate_limiter.throttled?(key, scope: [current_user], users_allowlist: rate_limit_users_allowlist)
rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
@@ -104,4 +103,8 @@ class Projects::NotesController < Projects::ApplicationController
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
+
+ def rate_limit_users_allowlist
+ Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist
+ end
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 463b989c493..a7c7839dc9f 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -9,7 +9,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
before_action do
- push_frontend_feature_flag(:vue_project_members_list, @project)
+ push_frontend_feature_flag(:vue_project_members_list, @project, default_enabled: :yaml)
end
feature_category :authentication_and_authorization
diff --git a/app/graphql/mutations/notes/create/base.rb b/app/graphql/mutations/notes/create/base.rb
index ad90e6598c1..a157a5abdf2 100644
--- a/app/graphql/mutations/notes/create/base.rb
+++ b/app/graphql/mutations/notes/create/base.rb
@@ -57,12 +57,18 @@ module Mutations
end
def verify_rate_limit!(current_user)
- rate_limiter, key = ::Gitlab::ApplicationRateLimiter, :notes_create
- return unless rate_limiter.throttled?(key, scope: [current_user])
+ return unless rate_limit_throttled?
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
'This endpoint has been requested too many times. Try again later.'
end
+
+ def rate_limit_throttled?
+ rate_limiter = ::Gitlab::ApplicationRateLimiter
+ allowlist = Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist
+
+ rate_limiter.throttled?(:notes_create, scope: [current_user], users_allowlist: allowlist)
+ end
end
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 2a1652cf2ba..8268ab1ad56 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -280,7 +280,7 @@ module ApplicationHelper
def page_class
class_names = []
- class_names << 'issue-boards-page' if current_controller?(:boards)
+ class_names << 'issue-boards-page gl-overflow-hidden' if current_controller?(:boards)
class_names << 'environment-logs-page' if current_controller?(:logs)
class_names << 'with-performance-bar' if performance_bar_enabled?
class_names << system_message_class
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index f92011958dc..b3b90c79076 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -329,6 +329,7 @@ module ApplicationSettingsHelper
:email_restrictions,
:issues_create_limit,
:notes_create_limit,
+ :notes_create_limit_allowlist_raw,
:raw_blob_request_limit,
:project_import_limit,
:project_export_limit,
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index db286005ff4..6d375a19ffb 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -447,6 +447,10 @@ class ApplicationSetting < ApplicationRecord
validates :notes_create_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :notes_create_limit_allowlist,
+ length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
+ allow_nil: false
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 9d99b638af6..e5284d15a49 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -93,7 +93,6 @@ module ApplicationSettingImplementation
import_sources: Settings.gitlab['import_sources'],
invisible_captcha_enabled: false,
issues_create_limit: 300,
- notes_create_limit: 300,
local_markdown_version: 0,
login_recaptcha_protection_enabled: false,
max_artifacts_size: Settings.artifacts['max_size'],
@@ -101,6 +100,8 @@ module ApplicationSettingImplementation
max_import_size: 0,
minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH,
mirror_available: true,
+ notes_create_limit: 300,
+ notes_create_limit_allowlist: [],
notify_on_unknown_sign_in: true,
outbound_local_requests_whitelist: [],
password_authentication_enabled_for_git: true,
@@ -270,6 +271,14 @@ module ApplicationSettingImplementation
self.protected_paths = strings_to_array(values)
end
+ def notes_create_limit_allowlist_raw
+ array_to_string(self.notes_create_limit_allowlist)
+ end
+
+ def notes_create_limit_allowlist_raw=(values)
+ self.notes_create_limit_allowlist = strings_to_array(values).map(&:downcase)
+ end
+
def asset_proxy_allowlist=(values)
values = strings_to_array(values) if values.is_a?(String)
diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb
index f5e52c04944..e2d10cc7e78 100644
--- a/app/models/design_management/design.rb
+++ b/app/models/design_management/design.rb
@@ -228,17 +228,6 @@ module DesignManagement
project
end
- def immediately_before?(next_design)
- return false if next_design.relative_position <= relative_position
-
- interloper = self.class.on_issue(issue).where(
- "relative_position <@ int4range(?, ?, '()')",
- *[self, next_design].map(&:relative_position)
- )
-
- !interloper.exists?
- end
-
def notes_with_associations
notes.includes(:author)
end
diff --git a/app/services/design_management/move_designs_service.rb b/app/services/design_management/move_designs_service.rb
index ca715b10351..129f93edf5e 100644
--- a/app/services/design_management/move_designs_service.rb
+++ b/app/services/design_management/move_designs_service.rb
@@ -16,7 +16,6 @@ module DesignManagement
return error(:cannot_move) unless current_user.can?(:move_design, current_design)
return error(:no_neighbors) unless neighbors.present?
return error(:not_distinct) unless all_distinct?
- return error(:not_adjacent) if any_in_gap?
return error(:not_same_issue) unless all_same_issue?
move_nulls_to_end
@@ -54,12 +53,6 @@ module DesignManagement
ids.uniq.size == ids.size
end
- def any_in_gap?
- return false unless previous_design&.relative_position && next_design&.relative_position
-
- !previous_design.immediately_before?(next_design)
- end
-
def all_same_issue?
issue.designs.id_in(ids).count == ids.size
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index ffed0a957c2..8cf84e32e85 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -101,8 +101,30 @@ module MergeRequests
%w(title description).each do |action|
next unless @issuable_changes.key?(action)
- Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
+ # Track edits to title or description
+ #
+ merge_request_activity_counter
.public_send("track_#{action}_edit_action".to_sym, user: current_user) # rubocop:disable GitlabSecurity/PublicSend
+
+ # Track changes to Draft/WIP status
+ #
+ if action == "title"
+ old_title, new_title = @issuable_changes["title"]
+ old_title_wip = MergeRequest.work_in_progress?(old_title)
+ new_title_wip = MergeRequest.work_in_progress?(new_title)
+
+ if !old_title_wip && new_title_wip
+ # Marked as Draft/WIP
+ #
+ merge_request_activity_counter
+ .track_marked_as_draft_action(user: current_user)
+ elsif old_title_wip && !new_title_wip
+ # Unmarked as Draft/WIP
+ #
+ merge_request_activity_counter
+ .track_unmarked_as_draft_action(user: current_user)
+ end
+ end
end
end
diff --git a/app/views/admin/application_settings/_note_limits.html.haml b/app/views/admin/application_settings/_note_limits.html.haml
index 3045c967b00..9578da90170 100644
--- a/app/views/admin/application_settings/_note_limits.html.haml
+++ b/app/views/admin/application_settings/_note_limits.html.haml
@@ -5,5 +5,8 @@
.form-group
= f.label :notes_create_limit, _('Max requests per minute per user'), class: 'label-bold'
= f.number_field :notes_create_limit, class: 'form-control gl-form-input'
+ .form-group
+ = f.label :notes_create_limit_allowlist, _('List of users to be excluded from the limit'), class: 'label-bold'
+ = f.text_area :notes_create_limit_allowlist_raw, placeholder: 'username1, username2', class: 'form-control gl-form-input', rows: 5
= f.submit _('Save changes'), class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index e4517dca6d0..c2599238bce 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -31,7 +31,7 @@
= render 'shared/group_tips'
.form-actions
= f.submit _('Create group'), class: "gl-button btn btn-success"
- = link_to _('Cancel'), admin_groups_path, class: "gl-button btn btn-cancel"
+ = link_to _('Cancel'), admin_groups_path, class: "gl-button btn btn-default btn-cancel"
- else
.form-actions
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 67e6e510923..705fd9bbd0f 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -7,7 +7,7 @@
.d-flex.justify-content-between.flex-wrap
- providers.each do |provider|
- has_icon = provider_has_icon?(provider)
- = button_to omniauth_authorize_path(:user, provider), id: "oauth-login-#{provider}", class: "gl-button btn d-flex align-items-center omniauth-btn text-left oauth-login #{qa_class_for_provider(provider)}" do
+ = button_to omniauth_authorize_path(:user, provider), id: "oauth-login-#{provider}", class: "btn gl-button btn-default d-flex align-items-center omniauth-btn text-left oauth-login #{qa_class_for_provider(provider)}" do
- if has_icon
= provider_image_tag(provider)
%span
diff --git a/app/views/devise/shared/_signup_omniauth_provider_list.haml b/app/views/devise/shared/_signup_omniauth_provider_list.haml
index ece886b3cdd..43e0802ee2a 100644
--- a/app/views/devise/shared/_signup_omniauth_provider_list.haml
+++ b/app/views/devise/shared/_signup_omniauth_provider_list.haml
@@ -2,7 +2,7 @@
= _("Create an account using:")
.d-flex.justify-content-between.flex-wrap
- providers.each do |provider|
- = link_to omniauth_authorize_path(:user, provider), method: :post, class: "btn gl-button gl-display-flex gl-align-items-center gl-text-left gl-mb-2 gl-p-2 omniauth-btn oauth-login #{qa_class_for_provider(provider)}", id: "oauth-login-#{provider}" do
+ = link_to omniauth_authorize_path(:user, provider), method: :post, class: "btn gl-button btn-default gl-display-flex gl-align-items-center gl-text-left gl-mb-2 gl-p-2 omniauth-btn oauth-login #{qa_class_for_provider(provider)}", id: "oauth-login-#{provider}" do
- if provider_has_icon?(provider)
= provider_image_tag(provider)
%span.ml-2
diff --git a/app/views/groups/_new_group_fields.html.haml b/app/views/groups/_new_group_fields.html.haml
index 3872bbcd062..64860c61082 100644
--- a/app/views/groups/_new_group_fields.html.haml
+++ b/app/views/groups/_new_group_fields.html.haml
@@ -18,5 +18,5 @@
= render_if_exists 'shared/groups/invite_members'
.row
.form-actions.col-sm-12
- = f.submit _('Create group'), class: "btn btn-success"
- = link_to _('Cancel'), dashboard_groups_path, class: 'btn btn-cancel'
+ = f.submit _('Create group'), class: "btn gl-button btn-success"
+ = link_to _('Cancel'), dashboard_groups_path, class: 'btn gl-button btn-default btn-cancel'
diff --git a/app/views/profiles/_email_settings.html.haml b/app/views/profiles/_email_settings.html.haml
index c05d42a5846..977116af88f 100644
--- a/app/views/profiles/_email_settings.html.haml
+++ b/app/views/profiles/_email_settings.html.haml
@@ -4,7 +4,7 @@
- read_only_help_text = readonly ? s_("Profiles|Your email address was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:email) } : user_email_help_text(@user)
- help_text = email_change_disabled ? s_("Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO.") % { group_name: @user.managing_group.name } : read_only_help_text
-= form.text_field :email, required: true, class: 'input-lg', value: (@user.email unless @user.temp_oauth_email?), help: help_text.html_safe, readonly: readonly || email_change_disabled
+= form.text_field :email, required: true, class: 'input-lg gl-form-input', value: (@user.email unless @user.temp_oauth_email?), help: help_text.html_safe, readonly: readonly || email_change_disabled
= form.select :public_email, options_for_select(@user.public_verified_emails, selected: @user.public_email),
{ help: s_("Profiles|This email will be displayed on your public profile"), include_blank: s_("Profiles|Do not show on profile") },
control_class: 'select2 input-lg', disabled: email_change_disabled
diff --git a/app/views/profiles/_name.html.haml b/app/views/profiles/_name.html.haml
index 87f1634b4f3..aea38bf4c3b 100644
--- a/app/views/profiles/_name.html.haml
+++ b/app/views/profiles/_name.html.haml
@@ -1,5 +1,5 @@
- if user.read_only_attribute?(:name)
- = form.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9 qa-full-name rspec-full-name' },
+ = form.text_field :name, class: 'gl-form-input', required: true, readonly: true, wrapper: { class: 'col-md-9 qa-full-name rspec-full-name' },
help: s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you") % { provider_label: attribute_provider_label(:name) }
- else
- = form.text_field :name, label: s_('Profiles|Full name'), required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name rspec-full-name' }, help: s_("Profiles|Enter your name, so people you know can recognize you")
+ = form.text_field :name, class: 'gl-form-input', label: s_('Profiles|Full name'), required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name rspec-full-name' }, help: s_("Profiles|Enter your name, so people you know can recognize you")
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index b1f4966f731..4689fd5272a 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -65,7 +65,7 @@
= status_form.hidden_field :emoji, id: 'js-status-emoji-field'
= status_form.text_field :message,
id: 'js-status-message-field',
- class: 'form-control input-lg',
+ class: 'form-control gl-form-input input-lg',
label: s_("Profiles|Your status"),
prepend: emoji_button,
append: reset_message_button,
@@ -100,20 +100,20 @@
.col-lg-8
.row
= render 'profiles/name', form: f, user: @user
- = f.text_field :id, readonly: true, label: s_('Profiles|User ID'), wrapper: { class: 'col-md-3' }
+ = f.text_field :id, class: 'gl-form-input', readonly: true, label: s_('Profiles|User ID'), wrapper: { class: 'col-md-3' }
= render_if_exists 'profiles/email_settings', form: f
- = f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username")
- = f.text_field :linkedin, class: 'input-md', help: s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename")
- = f.text_field :twitter, class: 'input-md', placeholder: s_("Profiles|@username")
- = f.text_field :website_url, class: 'input-lg', placeholder: s_("Profiles|website.com")
+ = f.text_field :skype, class: 'input-md gl-form-input', placeholder: s_("Profiles|username")
+ = f.text_field :linkedin, class: 'input-md gl-form-input', help: s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename")
+ = f.text_field :twitter, class: 'input-md gl-form-input', placeholder: s_("Profiles|@username")
+ = f.text_field :website_url, class: 'input-lg gl-form-input', placeholder: s_("Profiles|website.com")
- if @user.read_only_attribute?(:location)
- = f.text_field :location, readonly: true, help: s_("Profiles|Your location was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:location) }
+ = f.text_field :location, class: 'gl-form-input', readonly: true, help: s_("Profiles|Your location was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:location) }
- else
- = f.text_field :location, label: s_('Profiles|Location'), class: 'input-lg', placeholder: s_("Profiles|City, country")
- = f.text_field :job_title, class: 'input-md'
- = f.text_field :organization, label: s_('Profiles|Organization'), class: 'input-md', help: s_("Profiles|Who you represent or work for")
- = f.text_area :bio, label: s_('Profiles|Bio'), rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters")
+ = f.text_field :location, label: s_('Profiles|Location'), class: 'input-lg gl-form-input', placeholder: s_("Profiles|City, country")
+ = f.text_field :job_title, class: 'input-md gl-form-input'
+ = f.text_field :organization, label: s_('Profiles|Organization'), class: 'input-md gl-form-input', help: s_("Profiles|Who you represent or work for")
+ = f.text_area :bio, class: 'gl-form-input', label: s_('Profiles|Bio'), rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters")
%hr
%h5= s_("Private profile")
.checkbox-icon-inline-wrapper
diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml
index d1ff52548cd..7eb86e6ba3f 100644
--- a/app/views/projects/_project_templates.html.haml
+++ b/app/views/projects/_project_templates.html.haml
@@ -1,6 +1,6 @@
- f ||= local_assigns[:f]
-.project-templates-buttons.col-sm-12
+.project-templates-buttons
%ul.nav-tabs.nav-links.nav.scrolling-tabs
%li.built-in-tab
%a.nav-link.active{ href: "#built-in", data: { toggle: 'tab'} }
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index b3c209d564b..beb435d268a 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,6 +1,6 @@
- page_title _("Members")
- group = @project.group
-- vue_project_members_list_enabled = Feature.enabled?(:vue_project_members_list, @project)
+- vue_project_members_list_enabled = Feature.enabled?(:vue_project_members_list, @project, default_enabled: :yaml)
.js-remove-member-modal
.row.gl-mt-3
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index c70c0572c2b..95d7f075964 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -29,7 +29,7 @@
%ul
- if label.project_label? && label.project.group && can?(current_user, :admin_label, label.project.group)
%li
- %button.js-promote-project-label-button.btn.btn-transparent.btn-action{ disabled: true, type: 'button',
+ %button.js-promote-project-label-button.btn.btn-transparent{ disabled: true, type: 'button',
data: { url: promote_project_label_path(label.project, label),
label_title: label.title,
label_color: label.color,
diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
index 132a951fd34..1a22a66d185 100644
--- a/app/views/shared/issuable/_board_create_list_dropdown.html.haml
+++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
@@ -1,5 +1,5 @@
-.dropdown.gl-ml-3#js-add-list
- %button.gl-button.btn.btn-success.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
+.dropdown.gl-display-flex.gl-align-items-center.gl-ml-3#js-add-list
+ %button.gl-button.btn.btn-confirm.btn-confirm-secondary.js-new-board-list{ type: "button", data: board_list_data }
Add list
.dropdown-menu.dropdown-extended-height.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 1ebb160e591..d1e74cc771e 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -193,6 +193,8 @@
.filter-dropdown-container.d-flex.flex-column.flex-md-row
- if type == :boards
#js-board-labels-toggle
+ - if current_user
+ #js-board-epics-swimlanes-toggle
.js-board-config{ data: { can_admin_list: user_can_admin_list, has_scope: board.scoped? } }
- if user_can_admin_list
- if Feature.enabled?(:board_new_list, board.resource_parent, default_enabled: :yaml)
@@ -200,9 +202,7 @@
- else
= render 'shared/issuable/board_create_list_dropdown', board: board
- if @project
- #js-add-issues-btn.gl-ml-3{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
- - if current_user
- #js-board-epics-swimlanes-toggle
+ #js-add-issues-btn{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
#js-toggle-focus-btn
- elsif is_not_boards_modal_or_productivity_analytics && show_sorting_dropdown
= render 'shared/issuable/sort_dropdown'
diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml
index 1f05dcf83bc..a867421298b 100644
--- a/app/views/shared/issuable/_sidebar_todo.html.haml
+++ b/app/views/shared/issuable/_sidebar_todo.html.haml
@@ -12,4 +12,4 @@
data: todo_button_data }
%span.issuable-todo-inner.js-issuable-todo-inner<
= is_collapsed ? button_icon : button_title
- = loading_icon
+ = loading_icon(css_class: is_collapsed ? '' : 'gl-ml-3')
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index a419e749f35..d2bee57992d 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -8,7 +8,7 @@
= markdown_field(label, :description)
.float-right.d-none.d-lg-block
- = link_to milestones_issues_path(options.merge(state: 'opened')), class: 'btn gl-button btn-default-tertiary btn-action' do
+ = link_to milestones_issues_path(options.merge(state: 'opened')), class: 'btn gl-button btn-default-tertiary' do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), _('open issue')
- = link_to milestones_issues_path(options.merge(state: 'closed')), class: 'btn gl-button btn-default-tertiary btn-action' do
+ = link_to milestones_issues_path(options.merge(state: 'closed')), class: 'btn gl-button btn-default-tertiary' do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), _('closed issue')