diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
index c1a79134136..ca37f888ab9 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -32,6 +32,11 @@ export default {
required: false,
default: undefined,
},
+ category: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
triggerSource: {
type: String,
required: true,
@@ -82,6 +87,7 @@ export default {
v-if="isButtonTrigger"
v-bind="componentAttributes"
:variant="variant"
+ :category="category"
:icon="icon"
@click="openModal"
>
diff --git a/app/assets/javascripts/merge_requests/components/reviewers/reviewer_dropdown.vue b/app/assets/javascripts/merge_requests/components/reviewers/reviewer_dropdown.vue
index 0d85c5dba7d..ea9bb7ee887 100644
--- a/app/assets/javascripts/merge_requests/components/reviewers/reviewer_dropdown.vue
+++ b/app/assets/javascripts/merge_requests/components/reviewers/reviewer_dropdown.vue
@@ -6,6 +6,7 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import userAutocompleteWithMRPermissionsQuery from '~/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql';
+import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import UpdateReviewers from './update_reviewers.vue';
import userPermissionsQuery from './queries/user_permissions.query.graphql';
@@ -27,8 +28,9 @@ export default {
GlAvatar,
GlIcon,
UpdateReviewers,
+ InviteMembersTrigger,
},
- inject: ['projectPath', 'issuableId', 'issuableIid'],
+ inject: ['projectPath', 'issuableId', 'issuableIid', 'directlyInviteMembers'],
props: {
users: {
type: Array,
@@ -125,6 +127,13 @@ export default {
this.fetchedUsers = users;
this.searching = false;
},
+ removeAllReviewers() {
+ this.currentSelectedReviewers = [];
+ },
+ },
+ i18n: {
+ selectReviewer: __('Select reviewer'),
+ unassign: __('Unassign'),
},
};
@@ -138,8 +147,10 @@ export default {
@@ -168,10 +181,25 @@ export default {
{{ item.text }}
- {{ item.secondaryText }}
+ {{ item.secondaryText }}
+
+
+
+
+
+
diff --git a/app/assets/javascripts/ml/model_registry/components/import_artifact_zone.vue b/app/assets/javascripts/ml/model_registry/components/import_artifact_zone.vue
index 7840ee50686..e3bff29f948 100644
--- a/app/assets/javascripts/ml/model_registry/components/import_artifact_zone.vue
+++ b/app/assets/javascripts/ml/model_registry/components/import_artifact_zone.vue
@@ -13,7 +13,6 @@ import { joinPaths } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import { uploadModel } from '../services/upload_model';
-import { emptyArtifactFile } from '../constants';
export default {
name: 'ImportArtifactZone',
@@ -42,16 +41,11 @@ export default {
required: false,
default: true,
},
- value: {
- type: Object,
- required: false,
- default: () => emptyArtifactFile,
- },
},
data() {
return {
- file: this.value.file,
- subfolder: this.value.subfolder,
+ file: null,
+ subfolder: '',
alert: null,
progressLoaded: null,
progressTotal: null,
@@ -87,7 +81,7 @@ export default {
this.progressTotal = progressEvent.total;
this.progressLoaded = progressEvent.loaded;
},
- submitRequest(importPath) {
+ uploadArtifact(importPath) {
this.progressLoaded = 0;
this.progressTotal = this.file.size;
uploadModel({
@@ -107,19 +101,14 @@ export default {
this.alert = { message: error, variant: 'danger' };
});
},
- emitInput(value) {
- this.$emit('input', { ...value });
- },
changeSubfolder(subfolder) {
this.subfolder = subfolder;
- this.emitInput({ file: this.file, subfolder });
},
- uploadFile(file) {
+ changeFile(file) {
this.file = file;
- this.emitInput({ file, subfolder: this.subfolder });
if (this.submitOnSelect && this.path) {
- this.submitRequest(this.path);
+ this.uploadArtifact(this.path);
}
},
hideAlert() {
@@ -128,7 +117,6 @@ export default {
discardFile() {
this.file = null;
this.subfolder = '';
- this.emitInput(emptyArtifactFile);
},
},
i18n: {
@@ -188,7 +176,7 @@ export default {
:upload-single-message="$options.i18n.uploadSingleMessage"
:drop-to-start-message="$options.i18n.dropToStartMessage"
:is-file-valid="() => true"
- @change="uploadFile"
+ @change="changeFile"
>
diff --git a/app/assets/javascripts/ml/model_registry/components/model_create.vue b/app/assets/javascripts/ml/model_registry/components/model_create.vue
index 6e93c7d4587..9e428777bc2 100644
--- a/app/assets/javascripts/ml/model_registry/components/model_create.vue
+++ b/app/assets/javascripts/ml/model_registry/components/model_create.vue
@@ -14,10 +14,9 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
import { semverRegex, noSpacesRegex } from '~/lib/utils/regexp';
-import { uploadModel } from '../services/upload_model';
import createModelVersionMutation from '../graphql/mutations/create_model_version.mutation.graphql';
import createModelMutation from '../graphql/mutations/create_model.mutation.graphql';
-import { emptyArtifactFile, MODEL_CREATION_MODAL_ID } from '../constants';
+import { MODEL_CREATION_MODAL_ID } from '../constants';
export default {
name: 'ModelCreate',
@@ -49,7 +48,6 @@ export default {
description: '',
versionDescription: '',
errorMessage: null,
- selectedFile: emptyArtifactFile,
modelData: null,
versionData: null,
markdownDocPath: helpPagePath('user/markdown'),
@@ -137,22 +135,14 @@ export default {
this.versionData = await this.createModelVersion(this.modelData.mlModelCreate.model.id);
}
const versionErrors = this.versionData?.mlModelVersionCreate?.errors || [];
-
if (versionErrors.length) {
this.errorMessage = versionErrors.join(', ');
this.versionData = null;
} else {
// Attempt importing model artifacts
- const { importPath } = this.versionData.mlModelVersionCreate.modelVersion._links;
- await uploadModel({
- importPath,
- file: this.selectedFile.file,
- subfolder: this.selectedFile.subfolder,
- maxAllowedFileSize: this.maxAllowedFileSize,
- onUploadProgress: this.$refs.importArtifactZoneRef.onUploadProgress,
- });
-
- const { showPath } = this.versionData.mlModelVersionCreate.modelVersion._links;
+ const { showPath, importPath } =
+ this.versionData.mlModelVersionCreate.modelVersion._links;
+ await this.$refs.importArtifactZoneRef.uploadArtifact(importPath);
visitUrl(showPath);
}
} else {
@@ -162,7 +152,6 @@ export default {
} catch (error) {
Sentry.captureException(error);
this.errorMessage = error;
- this.selectedFile = emptyArtifactFile;
}
},
resetModal() {
@@ -171,7 +160,6 @@ export default {
this.description = '';
this.versionDescription = '';
this.errorMessage = null;
- this.selectedFile = emptyArtifactFile;
this.modelData = null;
this.versionData = null;
},
@@ -341,7 +329,6 @@ export default {
diff --git a/app/assets/javascripts/ml/model_registry/components/model_edit.vue b/app/assets/javascripts/ml/model_registry/components/model_edit.vue
index 84fce4edecf..af168d03383 100644
--- a/app/assets/javascripts/ml/model_registry/components/model_edit.vue
+++ b/app/assets/javascripts/ml/model_registry/components/model_edit.vue
@@ -16,7 +16,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { noSpacesRegex } from '~/lib/utils/regexp';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import editModelMutation from '../graphql/mutations/edit_model.mutation.graphql';
-import { emptyArtifactFile, MODEL_EDIT_MODAL_ID } from '../constants';
+import { MODEL_EDIT_MODAL_ID } from '../constants';
export default {
name: 'ModelEdit',
@@ -98,7 +98,6 @@ export default {
} catch (error) {
Sentry.captureException(error);
this.errorMessage = error;
- this.selectedFile = emptyArtifactFile;
}
},
resetModal() {
diff --git a/app/assets/javascripts/ml/model_registry/components/model_version_create.vue b/app/assets/javascripts/ml/model_registry/components/model_version_create.vue
index de5eba99135..7dac78c268f 100644
--- a/app/assets/javascripts/ml/model_registry/components/model_version_create.vue
+++ b/app/assets/javascripts/ml/model_registry/components/model_version_create.vue
@@ -14,9 +14,8 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { semverRegex } from '~/lib/utils/regexp';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { uploadModel } from '../services/upload_model';
import createModelVersionMutation from '../graphql/mutations/create_model_version.mutation.graphql';
-import { emptyArtifactFile, MODEL_VERSION_CREATION_MODAL_ID } from '../constants';
+import { MODEL_VERSION_CREATION_MODAL_ID } from '../constants';
export default {
name: 'ModelVersionCreate',
@@ -50,7 +49,6 @@ export default {
version: null,
description: '',
errorMessage: null,
- selectedFile: emptyArtifactFile,
versionData: null,
submitButtonDisabled: true,
markdownDocPath: helpPagePath('user/markdown'),
@@ -128,29 +126,20 @@ export default {
this.errorMessage = errors.join(', ');
this.versionData = null;
} else {
- const { importPath } = this.versionData.mlModelVersionCreate.modelVersion._links;
-
- await uploadModel({
- importPath,
- file: this.selectedFile.file,
- subfolder: this.selectedFile.subfolder,
- maxAllowedFileSize: this.maxAllowedFileSize,
- onUploadProgress: this.$refs.importArtifactZoneRef.onUploadProgress,
- });
- const { showPath } = this.versionData.mlModelVersionCreate.modelVersion._links;
+ const { showPath, importPath } =
+ this.versionData.mlModelVersionCreate.modelVersion._links;
+ await this.$refs.importArtifactZoneRef.uploadArtifact(importPath);
visitUrl(showPath);
}
} catch (error) {
Sentry.captureException(error);
this.errorMessage = error;
- this.selectedFile = emptyArtifactFile;
}
},
resetModal() {
this.version = null;
this.description = '';
this.errorMessage = null;
- this.selectedFile = emptyArtifactFile;
this.versionData = null;
},
hideAlert() {
@@ -251,7 +240,6 @@ export default {
diff --git a/app/assets/javascripts/ml/model_registry/constants.js b/app/assets/javascripts/ml/model_registry/constants.js
index 4639faa33e9..e26752427ef 100644
--- a/app/assets/javascripts/ml/model_registry/constants.js
+++ b/app/assets/javascripts/ml/model_registry/constants.js
@@ -27,8 +27,3 @@ export const MLFLOW_USAGE_MODAL_ID = 'model-registry-mlflow-usage-modal';
export const MODEL_VERSION_CREATION_MODAL_ID = 'create-model-version-modal';
export const MODEL_CREATION_MODAL_ID = 'create-model-modal';
export const MODEL_EDIT_MODAL_ID = 'edit-model-modal';
-
-export const emptyArtifactFile = {
- file: null,
- subfolder: '',
-};
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
index e5b6167f0a3..74547239237 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
@@ -4,7 +4,7 @@
import { MountingPortal } from 'portal-vue';
import { GlLoadingIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { n__ } from '~/locale';
+import { n__, s__ } from '~/locale';
import ReviewerDrawer from '~/merge_requests/components/reviewers/reviewer_drawer.vue';
export default {
@@ -52,6 +52,11 @@ export default {
this.drawerOpen = drawerOpen;
},
},
+ i18n: {
+ addEditReviewers: s__('MergeRequest|Add or edit reviewers'),
+ changeReviewer: s__('MergeRequest|Change reviewer'),
+ quickAdd: s__('MergeRequest|Quick add reviewers'),
+ },
};
@@ -64,7 +69,9 @@ export default {
createElement(SidebarReviewers, {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 797d03de26c..0e605d95846 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -284,6 +284,10 @@
}
}
+.reviewers-dropdown .gl-new-dropdown-panel {
+ min-width: $right-sidebar-width - $gl-padding;
+}
+
.merge-request-approved-icon {
animation: approval-animate 350ms ease-in;
}
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index e715dfb854d..37eef1b53ed 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -277,7 +277,7 @@ module IssuablesHelper
end
end
- def issuable_sidebar_options(issuable)
+ def issuable_sidebar_options(issuable, project)
{
endpoint: "#{issuable[:issuable_json_path]}?serializer=sidebar_extras",
toggleSubscriptionEndpoint: issuable[:toggle_subscription_path],
@@ -293,7 +293,8 @@ module IssuablesHelper
timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours,
canCreateTimelogs: issuable.dig(:current_user, :can_create_timelogs),
createNoteEmail: issuable[:create_note_email],
- issuableType: issuable[:type]
+ issuableType: issuable[:type],
+ directlyInviteMembers: can_admin_project_member?(project).to_s
}
end
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 277a444da96..0495b2cad3f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -84,4 +84,4 @@
.js-sidebar-move-issue-block{ data: { project_full_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
-# haml-lint:disable InlineJavaScript
- %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe
+ %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar, @project).to_json.html_safe
diff --git a/doc/administration/moderate_users.md b/doc/administration/moderate_users.md
index aafd800fe41..0a77b79949d 100644
--- a/doc/administration/moderate_users.md
+++ b/doc/administration/moderate_users.md
@@ -119,7 +119,7 @@ The user's state is set to active and they consume a
[seat](../subscriptions/self_managed/index.md#billable-users).
NOTE:
-Users can also be unblocked using the [GitLab API](../api/users.md#unblock-user).
+Users can also be unblocked using the [GitLab API](../api/user_moderation.md#unblock-a-user).
The unblock option may be unavailable for LDAP users. To enable the unblock option,
the LDAP identity first needs to be deleted:
@@ -162,7 +162,7 @@ To deactivate a user:
The user receives an email notification that their account has been deactivated. After this email, they no longer receive notifications.
For more information, see [user deactivation emails](../administration/settings/email.md#user-deactivation-emails).
-To deactivate users with the GitLab API, see [deactivate user](../api/users.md#deactivate-user). For information about permanent user restrictions, see [block and unblock users](#block-and-unblock-users).
+To deactivate users with the GitLab API, see [deactivate user](../api/user_moderation.md#deactivate-a-user). For information about permanent user restrictions, see [block and unblock users](#block-and-unblock-users).
### Automatically deactivate dormant users
@@ -240,7 +240,7 @@ The user's state is set to active and they consume a
NOTE:
A deactivated user can also activate their account themselves by logging back in via the UI.
-Users can also be activated using the [GitLab API](../api/users.md#activate-user).
+Users can also be activated using the [GitLab API](../api/user_moderation.md#activate-a-user).
## Ban and unban users
diff --git a/doc/administration/review_abuse_reports.md b/doc/administration/review_abuse_reports.md
index 005f34f68ef..38a21fccca2 100644
--- a/doc/administration/review_abuse_reports.md
+++ b/doc/administration/review_abuse_reports.md
@@ -85,18 +85,7 @@ page:

-NOTE:
-Users can be [blocked](../api/users.md#block-user) and
-[unblocked](../api/users.md#unblock-user) using the GitLab API.
+## Related topics
-
+- [Moderate users (administration)](moderate_users.md)
+- [Review spam logs](review_spam_logs.md)
diff --git a/doc/administration/review_spam_logs.md b/doc/administration/review_spam_logs.md
index ce640f47455..27f5ae0eca3 100644
--- a/doc/administration/review_spam_logs.md
+++ b/doc/administration/review_spam_logs.md
@@ -37,6 +37,7 @@ You can resolve a spam log with one of the following effects:
| **Remove log** | The spam log is removed from the list. |
| **Trust user** | The user is trusted, and can create issues, notes, snippets, and merge requests without being blocked for spam. Spam logs are not created for trusted users. |
-NOTE:
-Users can be [blocked](../api/users.md#block-user) and
-[unblocked](../api/users.md#unblock-user) using the GitLab API.
+## Related topics
+
+- [Moderate users (administration)](moderate_users.md)
+- [Review abuse reports](review_abuse_reports.md)
diff --git a/doc/api/user_moderation.md b/doc/api/user_moderation.md
new file mode 100644
index 00000000000..fb626391093
--- /dev/null
+++ b/doc/api/user_moderation.md
@@ -0,0 +1,156 @@
+---
+stage: Govern
+group: Authentication
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# User moderation API
+
+DETAILS:
+**Tier:** Free, Premium, Ultimate
+**Offering:** Self-managed, GitLab Dedicated
+
+You can [activate, ban, and block users](../administration/moderate_users.md) by using the REST API.
+
+## Activate a user
+
+Activate the specified user.
+
+Prerequisites:
+
+- You must be an administrator.
+
+```plaintext
+POST /users/:id/activate
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|----------------------|
+| `id` | integer | yes | ID of specified user |
+
+Returns:
+
+- `201 OK` on success.
+- `404 User Not Found` if the user cannot be found.
+- `403 Forbidden` if the user cannot be activated because they are blocked by an administrator or by LDAP synchronization.
+
+## Deactivate a user
+
+Deactivate the specified user.
+
+Prerequisites:
+
+- You must be an administrator.
+
+```plaintext
+POST /users/:id/deactivate
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|----------------------|
+| `id` | integer | yes | ID of specified user |
+
+Returns:
+
+- `201 OK` on success.
+- `404 User Not Found` if user cannot be found.
+- `403 Forbidden` when trying to deactivate a user that is:
+ - Blocked by administrator or by LDAP synchronization.
+ - Not [dormant](../administration/moderate_users.md#automatically-deactivate-dormant-users).
+ - Internal.
+
+## Block a user
+
+Block the specified user.
+
+Prerequisites:
+
+- You must be an administrator.
+
+```plaintext
+POST /users/:id/block
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|----------------------|
+| `id` | integer | yes | ID of specified user |
+
+Returns:
+
+- `201 OK` on success.
+- `404 User Not Found` if user cannot be found.
+- `403 Forbidden` when trying to block:
+ - A user that is blocked through LDAP.
+ - An internal user.
+
+## Unblock a user
+
+Unblock the specified user.
+
+Prerequisites:
+
+- You must be an administrator.
+
+```plaintext
+POST /users/:id/unblock
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|----------------------|
+| `id` | integer | yes | ID of specified user |
+
+Returns `201 OK` on success, `404 User Not Found` is user cannot be found or
+`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
+
+## Ban a user
+
+Ban the specified user.
+
+Prerequisites:
+
+- You must be an administrator.
+
+```plaintext
+POST /users/:id/ban
+```
+
+Parameters:
+
+- `id` (required) - ID of specified user
+
+Returns:
+
+- `201 OK` on success.
+- `404 User Not Found` if user cannot be found.
+- `403 Forbidden` when trying to ban a user that is not active.
+
+## Unban a user
+
+Unban the specified user. Available only for administrator.
+
+```plaintext
+POST /users/:id/unban
+```
+
+Parameters:
+
+- `id` (required) - ID of specified user
+
+Returns:
+
+- `201 OK` on success.
+- `404 User Not Found` if the user cannot be found.
+- `403 Forbidden` when trying to unban a user that is not banned.
+
+## Related topics
+
+- [Review abuse reports](../administration/review_abuse_reports.md)
+- [Review spam logs](../administration/review_spam_logs.md)
diff --git a/doc/api/users.md b/doc/api/users.md
index 45be9ad38e7..d81164ecd75 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -1873,156 +1873,6 @@ Parameters:
| `id` | integer | yes | ID of specified user |
| `email_id` | integer | yes | Email ID |
-## Block user
-
-DETAILS:
-**Tier:** Free, Premium, Ultimate
-**Offering:** Self-managed, GitLab Dedicated
-
-Blocks the specified user. Available only for administrator.
-
-```plaintext
-POST /users/:id/block
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-|------------|---------|----------|----------------------|
-| `id` | integer | yes | ID of specified user |
-
-Returns:
-
-- `201 OK` on success.
-- `404 User Not Found` if user cannot be found.
-- `403 Forbidden` when trying to block:
- - A user that is blocked through LDAP.
- - An internal user.
-
-## Unblock user
-
-DETAILS:
-**Tier:** Free, Premium, Ultimate
-**Offering:** Self-managed, GitLab Dedicated
-
-Unblocks the specified user. Available only for administrator.
-
-```plaintext
-POST /users/:id/unblock
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-|------------|---------|----------|----------------------|
-| `id` | integer | yes | ID of specified user |
-
-Returns `201 OK` on success, `404 User Not Found` is user cannot be found or
-`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
-
-## Deactivate user
-
-DETAILS:
-**Tier:** Free, Premium, Ultimate
-**Offering:** Self-managed, GitLab Dedicated
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22257) in GitLab 12.4.
-
-Deactivates the specified user. Available only for administrator.
-
-```plaintext
-POST /users/:id/deactivate
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-|------------|---------|----------|----------------------|
-| `id` | integer | yes | ID of specified user |
-
-Returns:
-
-- `201 OK` on success.
-- `404 User Not Found` if user cannot be found.
-- `403 Forbidden` when trying to deactivate a user that is:
- - Blocked by administrator or by LDAP synchronization.
- - Not [dormant](../administration/moderate_users.md#automatically-deactivate-dormant-users).
- - Internal.
-
-## Activate user
-
-DETAILS:
-**Tier:** Free, Premium, Ultimate
-**Offering:** Self-managed, GitLab Dedicated
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22257) in GitLab 12.4.
-
-Activates the specified user. Available only for administrator.
-
-```plaintext
-POST /users/:id/activate
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-|------------|---------|----------|----------------------|
-| `id` | integer | yes | ID of specified user |
-
-Returns:
-
-- `201 OK` on success.
-- `404 User Not Found` if the user cannot be found.
-- `403 Forbidden` if the user cannot be activated because they are blocked by an administrator or by LDAP synchronization.
-
-## Ban user
-
-DETAILS:
-**Tier:** Free, Premium, Ultimate
-**Offering:** Self-managed, GitLab Dedicated
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/327354) in GitLab 14.3.
-
-Bans the specified user. Available only for administrator.
-
-```plaintext
-POST /users/:id/ban
-```
-
-Parameters:
-
-- `id` (required) - ID of specified user
-
-Returns:
-
-- `201 OK` on success.
-- `404 User Not Found` if user cannot be found.
-- `403 Forbidden` when trying to ban a user that is not active.
-
-## Unban user
-
-DETAILS:
-**Tier:** Free, Premium, Ultimate
-**Offering:** Self-managed, GitLab Dedicated
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/327354) in GitLab 14.3.
-
-Unbans the specified user. Available only for administrator.
-
-```plaintext
-POST /users/:id/unban
-```
-
-Parameters:
-
-- `id` (required) - ID of specified user
-
-Returns:
-
-- `201 OK` on success.
-- `404 User Not Found` if the user cannot be found.
-- `403 Forbidden` when trying to unban a user that is not banned.
-
## Get user contribution events
See the [Events API documentation](events.md#get-user-contribution-events)
diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md
index fcb402aa465..8c522b3c173 100644
--- a/doc/development/feature_flags/index.md
+++ b/doc/development/feature_flags/index.md
@@ -950,14 +950,6 @@ Feature.enabled?(:ci_live_trace) # => false
Feature.enabled?(:ci_live_trace, gate) # => true
```
-You can also disable a feature flag for a specific actor:
-
-```ruby
-gate = stub_feature_flag_gate('CustomActor')
-
-stub_feature_flags(ci_live_trace: false, thing: gate)
-```
-
### Controlling feature flags engine in tests
Our Flipper engine in the test environment works in a memory mode `Flipper::Adapters::Memory`.
diff --git a/lib/api/entities/clusters/agent_url_configuration.rb b/lib/api/entities/clusters/agent_url_configuration.rb
index fd4cb664bd7..90332043b49 100644
--- a/lib/api/entities/clusters/agent_url_configuration.rb
+++ b/lib/api/entities/clusters/agent_url_configuration.rb
@@ -15,19 +15,19 @@ module API
def public_key
return unless object.public_key
- Base64.encode64(object.public_key)
+ Base64.strict_encode64(object.public_key)
end
def client_cert
return unless object.client_cert
- Base64.encode64(object.client_cert)
+ Base64.strict_encode64(object.client_cert)
end
def ca_cert
return unless object.ca_cert
- Base64.encode64(object.ca_cert)
+ Base64.strict_encode64(object.ca_cert)
end
end
end
diff --git a/lib/api/entities/clusters/receptive_agent.rb b/lib/api/entities/clusters/receptive_agent.rb
index 7383a033e20..b4e95492a45 100644
--- a/lib/api/entities/clusters/receptive_agent.rb
+++ b/lib/api/entities/clusters/receptive_agent.rb
@@ -14,7 +14,7 @@ module API
def jwt
return unless object.private_key
- { private_key: Base64.encode64(object.private_key) }
+ { private_key: Base64.strict_encode64(object.private_key) }
end
def mtls
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index fb071dc78e4..825ce4a9bfa 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3353,9 +3353,6 @@ msgstr ""
msgid "Add new webhook"
msgstr ""
-msgid "Add or edit reviewers"
-msgstr ""
-
msgid "Add or remove a user."
msgstr ""
@@ -10879,9 +10876,6 @@ msgstr ""
msgid "Change path"
msgstr ""
-msgid "Change reviewer"
-msgstr ""
-
msgid "Change reviewers"
msgstr ""
@@ -33748,6 +33742,9 @@ msgstr ""
msgid "MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}"
msgstr ""
+msgid "MergeRequest|Add or edit reviewers"
+msgstr ""
+
msgid "MergeRequest|Awaiting review"
msgstr ""
@@ -33769,6 +33766,9 @@ msgstr ""
msgid "MergeRequest|Can't show this merge request because the target branch %{branch_badge} is missing from project %{path_badge}. Close this merge request or update the target branch."
msgstr ""
+msgid "MergeRequest|Change reviewer"
+msgstr ""
+
msgid "MergeRequest|Compare %{target} and %{source}"
msgstr ""
@@ -33784,6 +33784,9 @@ msgstr ""
msgid "MergeRequest|No files found"
msgstr ""
+msgid "MergeRequest|Quick add reviewers"
+msgstr ""
+
msgid "MergeRequest|Remove reviewer"
msgstr ""
@@ -57475,6 +57478,9 @@ msgstr ""
msgid "Unarchiving the project restores its members' ability to make commits, and create issues, comments, and other entities. %{strong_start}After you unarchive the project, it displays in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}"
msgstr ""
+msgid "Unassign"
+msgstr ""
+
msgid "Unassign from commenting user"
msgstr ""
diff --git a/spec/frontend/merge_requests/components/reviewers/reviewer_dropdown_spec.js b/spec/frontend/merge_requests/components/reviewers/reviewer_dropdown_spec.js
index 667c51a8792..3ca5fc9de95 100644
--- a/spec/frontend/merge_requests/components/reviewers/reviewer_dropdown_spec.js
+++ b/spec/frontend/merge_requests/components/reviewers/reviewer_dropdown_spec.js
@@ -77,6 +77,7 @@ function createComponent(
projectPath: 'gitlab-org/gitlab',
issuableId: '1',
issuableIid: '1',
+ directlyInviteMembers: true,
},
stubs: {
UpdateReviewers,
diff --git a/spec/frontend/ml/model_registry/components/model_create_spec.js b/spec/frontend/ml/model_registry/components/model_create_spec.js
index ef978a9a0dc..0604fd26edf 100644
--- a/spec/frontend/ml/model_registry/components/model_create_spec.js
+++ b/spec/frontend/ml/model_registry/components/model_create_spec.js
@@ -26,7 +26,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
}));
jest.mock('~/ml/model_registry/services/upload_model', () => ({
- uploadModel: jest.fn(),
+ uploadModel: jest.fn(() => Promise.resolve()),
}));
describe('ModelCreate', () => {
@@ -229,7 +229,6 @@ describe('ModelCreate', () => {
expect(findImportArtifactZone().props()).toEqual({
path: null,
submitOnSelect: false,
- value: { file: null, subfolder: '' },
});
});
@@ -397,9 +396,7 @@ describe('ModelCreate', () => {
});
});
- it('Visits the model versions page upon successful create mutation', async () => {
- createWrapper();
- await submitForm();
+ it('Visits the model versions page upon successful create mutation', () => {
expect(visitUrl).toHaveBeenCalledWith('/some/project/-/ml/models/1/versions/1');
});
});
@@ -414,9 +411,7 @@ describe('ModelCreate', () => {
await submitForm();
});
- it('Visits the model page upon successful create mutation without a version', async () => {
- createWrapper();
- await submitForm();
+ it('Visits the model page upon successful create mutation without a version', () => {
expect(visitUrl).toHaveBeenCalledWith('/some/project/-/ml/models/1');
});
});
@@ -489,7 +484,6 @@ describe('ModelCreate', () => {
});
it('Visits the model versions page upon successful create mutation', async () => {
- expect(findGlAlert().text()).toBe('Artifact import error.');
await submitForm(); // retry submit
expect(visitUrl).toHaveBeenCalledWith('/some/project/-/ml/models/1/versions/1');
});
diff --git a/spec/frontend/ml/model_registry/components/model_version_create_spec.js b/spec/frontend/ml/model_registry/components/model_version_create_spec.js
index a18564f444c..227309a0fb3 100644
--- a/spec/frontend/ml/model_registry/components/model_version_create_spec.js
+++ b/spec/frontend/ml/model_registry/components/model_version_create_spec.js
@@ -25,7 +25,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
}));
jest.mock('~/ml/model_registry/services/upload_model', () => ({
- uploadModel: jest.fn(),
+ uploadModel: jest.fn(() => Promise.resolve()),
}));
describe('ModelVersionCreate', () => {
@@ -124,7 +124,6 @@ describe('ModelVersionCreate', () => {
expect(findImportArtifactZone().props()).toEqual({
path: null,
submitOnSelect: false,
- value: { file: null, subfolder: '' },
});
});
@@ -297,29 +296,12 @@ describe('ModelVersionCreate', () => {
expect(findGlAlert().text()).toBe('Version is invalid');
});
- it('Displays an alert upon an exception', async () => {
- createWrapper();
- uploadModel.mockRejectedValueOnce('Runtime error');
-
- await submitForm();
-
- expect(findGlAlert().text()).toBe('Runtime error');
- });
-
- it('Logs to sentry upon an exception', async () => {
- createWrapper();
- uploadModel.mockRejectedValueOnce('Runtime error');
-
- await submitForm();
-
- expect(Sentry.captureException).toHaveBeenCalledWith('Runtime error');
- });
-
describe('Failed flow with file upload retried', () => {
beforeEach(async () => {
createWrapper();
findVersionInput().vm.$emit('input', '1.0.0');
zone().vm.$emit('change', file);
+ await nextTick();
uploadModel.mockRejectedValueOnce('Artifact import error.');
await submitForm();
diff --git a/spec/frontend/ml/model_registry/components/model_version_detail_spec.js b/spec/frontend/ml/model_registry/components/model_version_detail_spec.js
index 2271af075d2..4c9c6c09cea 100644
--- a/spec/frontend/ml/model_registry/components/model_version_detail_spec.js
+++ b/spec/frontend/ml/model_registry/components/model_version_detail_spec.js
@@ -88,10 +88,6 @@ describe('ml/model_registry/components/model_version_detail.vue', () => {
expect(findImportArtifactZone().props()).toEqual({
path: 'path/to/import',
submitOnSelect: true,
- value: {
- file: null,
- subfolder: '',
- },
});
});
diff --git a/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js b/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
index 357ddd993ea..898098cec75 100644
--- a/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
@@ -40,7 +40,7 @@ describe('ReviewerTitle component', () => {
reviewerAssignDrawer,
},
},
- stubs: ['approval-summary'],
+ stubs: ['approval-summary', 'ReviewerDropdown'],
});
};
@@ -89,7 +89,7 @@ describe('ReviewerTitle component', () => {
editable: false,
});
- expect(wrapper.vm.$el.querySelector('.edit-link')).toBeNull();
+ expect(findEditButton().exists()).toBe(false);
});
it('renders edit link when editable', () => {
@@ -98,7 +98,7 @@ describe('ReviewerTitle component', () => {
editable: true,
});
- expect(wrapper.vm.$el.querySelector('.edit-link')).not.toBeNull();
+ expect(findEditButton().exists()).toBe(true);
});
it('tracks the event when edit is clicked', () => {
diff --git a/spec/lib/api/entities/clusters/receptive_agent_spec.rb b/spec/lib/api/entities/clusters/receptive_agent_spec.rb
index 38858837f58..fc881b4ae2d 100644
--- a/spec/lib/api/entities/clusters/receptive_agent_spec.rb
+++ b/spec/lib/api/entities/clusters/receptive_agent_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe API::Entities::Clusters::ReceptiveAgent, feature_category: :deplo
it do
is_expected.to include(
id: config.agent_id,
- jwt: { private_key: Base64.encode64(config.private_key) }
+ jwt: { private_key: Base64.strict_encode64(config.private_key) }
)
end
end
diff --git a/spec/requests/api/clusters/agent_url_configurations_spec.rb b/spec/requests/api/clusters/agent_url_configurations_spec.rb
index dfbf356f4b9..8ecc78f164c 100644
--- a/spec/requests/api/clusters/agent_url_configurations_spec.rb
+++ b/spec/requests/api/clusters/agent_url_configurations_spec.rb
@@ -166,16 +166,16 @@ RSpec.describe API::Clusters::AgentUrlConfigurations, feature_category: :deploym
context 'when providing client cert and key' do
let_it_be(:client_cert) do
- Base64.encode64(File.read(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')))
+ File.read(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
end
- let_it_be(:client_key) { Base64.encode64(File.read(Rails.root.join('spec/fixtures/clusters/sample_key.key'))) }
+ let_it_be(:client_key) { File.read(Rails.root.join('spec/fixtures/clusters/sample_key.key')) }
let_it_be(:params) do
{
url: 'grpcs://localhost:4242',
- client_cert: client_cert,
- client_key: client_key
+ client_cert: Base64.encode64(client_cert),
+ client_key: Base64.encode64(client_key)
}
end
@@ -187,17 +187,17 @@ RSpec.describe API::Clusters::AgentUrlConfigurations, feature_category: :deploym
expect(json_response['agent_id']).to eq(agent.id)
expect(json_response['url']).to eq(params[:url])
expect(json_response['public_key']).to be_nil
- expect(json_response['client_cert']).to eq(client_cert)
+ expect(Base64.decode64(json_response['client_cert'])).to eq(client_cert)
end
end
context 'when providing ca cert' do
- let_it_be(:ca_cert) { Base64.encode64(File.read(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))) }
+ let_it_be(:ca_cert) { File.read(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) }
let_it_be(:params) do
{
url: 'grpcs://localhost:4242',
- ca_cert: ca_cert
+ ca_cert: Base64.encode64(ca_cert)
}
end
@@ -208,7 +208,7 @@ RSpec.describe API::Clusters::AgentUrlConfigurations, feature_category: :deploym
expect(response).to match_response_schema('public_api/v4/agent_url_configuration')
expect(json_response['agent_id']).to eq(agent.id)
expect(json_response['url']).to eq(params[:url])
- expect(json_response['ca_cert']).to eq(ca_cert)
+ expect(Base64.decode64(json_response['ca_cert'])).to eq(ca_cert)
end
end