diff --git a/Gemfile b/Gemfile index a8e18170d88..2b1785145fe 100644 --- a/Gemfile +++ b/Gemfile @@ -360,7 +360,7 @@ group :development, :test do gem 'awesome_print', require: false gem 'database_cleaner', '~> 1.7.0' - gem 'factory_bot_rails', '~> 6.1.0' + gem 'factory_bot_rails', '~> 6.2.0' gem 'rspec-rails', '~> 5.0.1' # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) diff --git a/Gemfile.lock b/Gemfile.lock index 4fc082bbe1b..fd328913638 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -340,10 +340,10 @@ GEM expression_parser (0.9.0) extended-markdown-filter (0.6.0) html-pipeline (~> 2.0) - factory_bot (6.1.0) + factory_bot (6.2.0) activesupport (>= 5.0.0) - factory_bot_rails (6.1.0) - factory_bot (~> 6.1.0) + factory_bot_rails (6.2.0) + factory_bot (~> 6.2.0) railties (>= 5.0.0) faraday (1.4.2) faraday-em_http (~> 1.0) @@ -728,7 +728,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.9.1) + loofah (2.11.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) lru_redux (1.1.0) @@ -748,7 +748,7 @@ GEM mini_histogram (0.3.1) mini_magick (4.10.1) mini_mime (1.0.2) - mini_portile2 (2.5.1) + mini_portile2 (2.5.3) minitest (5.11.3) mixlib-cli (2.1.8) mixlib-config (3.0.9) @@ -786,7 +786,7 @@ GEM netrc (0.11.0) nio4r (2.5.4) no_proxy_fix (0.1.2) - nokogiri (1.11.5) + nokogiri (1.11.7) mini_portile2 (~> 2.5.0) racc (~> 1.4) nokogumbo (2.0.2) @@ -1001,7 +1001,7 @@ GEM rake (>= 0.8.7) thor (~> 1.0) rainbow (3.0.0) - rake (13.0.3) + rake (13.0.6) rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) @@ -1449,7 +1449,7 @@ DEPENDENCIES email_spec (~> 2.2.0) erubi (~> 1.9.0) escape_utils (~> 1.1) - factory_bot_rails (~> 6.1.0) + factory_bot_rails (~> 6.2.0) faraday (~> 1.0) faraday_middleware-aws-sigv4 (~> 0.3.0) fast_blank diff --git a/app/assets/javascripts/content_editor/extensions/emoji.js b/app/assets/javascripts/content_editor/extensions/emoji.js new file mode 100644 index 00000000000..d88b9f92215 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/emoji.js @@ -0,0 +1,93 @@ +import { Node } from '@tiptap/core'; +import { InputRule } from 'prosemirror-inputrules'; +import { initEmojiMap, getAllEmoji } from '~/emoji'; + +export const emojiInputRegex = /(?:^|\s)((?::)((?:\w+))(?::))$/; + +export default Node.create({ + name: 'emoji', + + inline: true, + + group: 'inline', + + draggable: true, + + addAttributes() { + return { + moji: { + default: null, + parseHTML: (element) => { + return { + moji: element.textContent, + }; + }, + }, + name: { + default: null, + parseHTML: (element) => { + return { + name: element.dataset.name, + }; + }, + }, + title: { + default: null, + }, + unicodeVersion: { + default: '6.0', + parseHTML: (element) => { + return { + unicodeVersion: element.dataset.unicodeVersion, + }; + }, + }, + }; + }, + + parseHTML() { + return [ + { + tag: 'gl-emoji', + }, + ]; + }, + + renderHTML({ node }) { + return [ + 'gl-emoji', + { + 'data-name': node.attrs.name, + title: node.attrs.title, + 'data-unicode-version': node.attrs.unicodeVersion, + }, + node.attrs.moji, + ]; + }, + + addInputRules() { + return [ + new InputRule(emojiInputRegex, (state, match, start, end) => { + const [, , name] = match; + const emojis = getAllEmoji(); + const emoji = emojis[name]; + const { tr } = state; + + if (emoji) { + tr.replaceWith(start, end, [ + state.schema.text(' '), + this.type.create({ name, moji: emoji.e, unicodeVersion: emoji.u, title: emoji.d }), + ]); + + return tr; + } + + return null; + }), + ]; + }, + + onCreate() { + initEmojiMap(); + }, +}); diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js index 8f2ed3fcacd..eb6317bd988 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -9,6 +9,7 @@ import Code from '../extensions/code'; import CodeBlockHighlight from '../extensions/code_block_highlight'; import Document from '../extensions/document'; import Dropcursor from '../extensions/dropcursor'; +import Emoji from '../extensions/emoji'; import Gapcursor from '../extensions/gapcursor'; import HardBreak from '../extensions/hard_break'; import Heading from '../extensions/heading'; @@ -62,6 +63,7 @@ export const createContentEditor = ({ CodeBlockHighlight, Document, Dropcursor, + Emoji, Gapcursor, HardBreak, Heading, diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index 8b635e168ed..702344f5514 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -8,6 +8,7 @@ import Bold from '../extensions/bold'; import BulletList from '../extensions/bullet_list'; import Code from '../extensions/code'; import CodeBlockHighlight from '../extensions/code_block_highlight'; +import Emoji from '../extensions/emoji'; import HardBreak from '../extensions/hard_break'; import Heading from '../extensions/heading'; import HorizontalRule from '../extensions/horizontal_rule'; @@ -51,6 +52,11 @@ const defaultSerializerConfig = { [Blockquote.name]: defaultMarkdownSerializer.nodes.blockquote, [BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list, [CodeBlockHighlight.name]: defaultMarkdownSerializer.nodes.code_block, + [Emoji.name]: (state, node) => { + const { name } = node.attrs; + + state.write(`:${name}:`); + }, [HardBreak.name]: defaultMarkdownSerializer.nodes.hard_break, [Heading.name]: defaultMarkdownSerializer.nodes.heading, [HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule, diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue index e9559f735d6..bcacf9a3bbc 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue @@ -16,8 +16,7 @@ import { s__, __, n__ } from '~/locale'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import { STATUSES } from '../../constants'; import importGroupsMutation from '../graphql/mutations/import_groups.mutation.graphql'; -import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graphql'; -import setTargetNamespaceMutation from '../graphql/mutations/set_target_namespace.mutation.graphql'; +import setImportTargetMutation from '../graphql/mutations/set_import_target.mutation.graphql'; import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql'; import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql'; import ImportTableRow from './import_table_row.vue'; @@ -142,17 +141,10 @@ export default { this.page = page; }, - updateTargetNamespace(sourceGroupId, targetNamespace) { + updateImportTarget(sourceGroupId, targetNamespace, newName) { this.$apollo.mutate({ - mutation: setTargetNamespaceMutation, - variables: { sourceGroupId, targetNamespace }, - }); - }, - - updateNewName(sourceGroupId, newName) { - this.$apollo.mutate({ - mutation: setNewNameMutation, - variables: { sourceGroupId, newName }, + mutation: setImportTargetMutation, + variables: { sourceGroupId, targetNamespace, newName }, }); }, @@ -266,8 +258,12 @@ export default { :available-namespaces="availableNamespaces" :group-path-regex="groupPathRegex" :group-url-error-message="groupUrlErrorMessage" - @update-target-namespace="updateTargetNamespace(group.id, $event)" - @update-new-name="updateNewName(group.id, $event)" + @update-target-namespace=" + updateImportTarget(group.id, $event, group.import_target.new_name) + " + @update-new-name=" + updateImportTarget(group.id, group.import_target.target_namespace, $event) + " @import-group="importGroups([group.id])" /> diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue index 96c1dd21821..3dd780895a2 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue @@ -9,15 +9,9 @@ import { GlFormInput, } from '@gitlab/ui'; import { joinPaths } from '~/lib/utils/url_utility'; -import { s__ } from '~/locale'; import ImportGroupDropdown from '../../components/group_dropdown.vue'; import ImportStatus from '../../components/import_status.vue'; import { STATUSES } from '../../constants'; -import addValidationErrorMutation from '../graphql/mutations/add_validation_error.mutation.graphql'; -import removeValidationErrorMutation from '../graphql/mutations/remove_validation_error.mutation.graphql'; -import groupAndProjectQuery from '../graphql/queries/groupAndProject.query.graphql'; - -const DEBOUNCE_INTERVAL = 300; export default { components: { @@ -50,42 +44,6 @@ export default { }, }, - apollo: { - existingGroupAndProject: { - query: groupAndProjectQuery, - debounce: DEBOUNCE_INTERVAL, - variables() { - return { - fullPath: this.fullPath, - }; - }, - update({ existingGroup, existingProject }) { - const variables = { - field: 'new_name', - sourceGroupId: this.group.id, - }; - - if (!existingGroup && !existingProject) { - this.$apollo.mutate({ - mutation: removeValidationErrorMutation, - variables, - }); - } else { - this.$apollo.mutate({ - mutation: addValidationErrorMutation, - variables: { - ...variables, - message: this.$options.i18n.NAME_ALREADY_EXISTS, - }, - }); - } - }, - skip() { - return !this.isNameValid || this.isAlreadyImported; - }, - }, - }, - computed: { availableNamespaceNames() { return this.availableNamespaces.map((ns) => ns.full_path); @@ -123,10 +81,6 @@ export default { return joinPaths(gon.relative_url_root || '/', this.fullPath); }, }, - - i18n: { - NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'), - }, }; diff --git a/app/assets/javascripts/import_entities/import_groups/constants.js b/app/assets/javascripts/import_entities/import_groups/constants.js new file mode 100644 index 00000000000..d920e87aac8 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/constants.js @@ -0,0 +1,5 @@ +import { s__ } from '~/locale'; + +export const i18n = { + NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'), +}; diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js index 2cde3781a6a..c608aa164d1 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js @@ -4,11 +4,15 @@ import axios from '~/lib/utils/axios_utils'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; import { STATUSES } from '../../constants'; +import { i18n } from '../constants'; import bulkImportSourceGroupItemFragment from './fragments/bulk_import_source_group_item.fragment.graphql'; +import addValidationErrorMutation from './mutations/add_validation_error.mutation.graphql'; +import removeValidationErrorMutation from './mutations/remove_validation_error.mutation.graphql'; import setImportProgressMutation from './mutations/set_import_progress.mutation.graphql'; import updateImportStatusMutation from './mutations/update_import_status.mutation.graphql'; import availableNamespacesQuery from './queries/available_namespaces.query.graphql'; import bulkImportSourceGroupQuery from './queries/bulk_import_source_group.query.graphql'; +import groupAndProjectQuery from './queries/group_and_project.query.graphql'; import { SourceGroupsManager } from './services/source_groups_manager'; import { StatusPoller } from './services/status_poller'; import typeDefs from './typedefs.graphql'; @@ -46,6 +50,37 @@ function makeGroup(data) { return result; } +async function checkImportTargetIsValid({ client, newName, targetNamespace, sourceGroupId }) { + const { + data: { existingGroup, existingProject }, + } = await client.query({ + query: groupAndProjectQuery, + variables: { + fullPath: `${targetNamespace}/${newName}`, + }, + }); + + const variables = { + field: 'new_name', + sourceGroupId, + }; + + if (!existingGroup && !existingProject) { + client.mutate({ + mutation: removeValidationErrorMutation, + variables, + }); + } else { + client.mutate({ + mutation: addValidationErrorMutation, + variables: { + ...variables, + message: i18n.NAME_ALREADY_EXISTS, + }, + }); + } +} + const localProgressId = (id) => `not-started-${id}`; export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) { @@ -99,7 +134,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr ]) => { const pagination = parseIntPagination(normalizeHeaders(headers)); - return { + const response = { __typename: clientTypenames.BulkImportSourceGroupConnection, nodes: data.importable_data.map((group) => { const { jobId, importState: cachedImportState } = @@ -123,6 +158,21 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr ...pagination, }, }; + + setTimeout(() => { + response.nodes.forEach((group) => { + if (group.progress.status === STATUSES.NONE) { + checkImportTargetIsValid({ + client, + newName: group.import_target.new_name, + targetNamespace: group.import_target.target_namespace, + sourceGroupId: group.id, + }); + } + }); + }); + + return response; }, ); }, @@ -136,6 +186,22 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr ), }, Mutation: { + setImportTarget(_, { targetNamespace, newName, sourceGroupId }, { client }) { + checkImportTargetIsValid({ + client, + sourceGroupId, + targetNamespace, + newName, + }); + return makeGroup({ + id: sourceGroupId, + import_target: { + target_namespace: targetNamespace, + new_name: newName, + }, + }); + }, + setTargetNamespace: (_, { targetNamespace, sourceGroupId }) => makeGroup({ id: sourceGroupId, diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql new file mode 100644 index 00000000000..793b60ee378 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql @@ -0,0 +1,13 @@ +mutation setImportTarget($newName: String!, $targetNamespace: String!, $sourceGroupId: String!) { + setImportTarget( + newName: $newName + targetNamespace: $targetNamespace + sourceGroupId: $sourceGroupId + ) @client { + id + import_target { + new_name + target_namespace + } + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql deleted file mode 100644 index 354bf2a5815..00000000000 --- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql +++ /dev/null @@ -1,8 +0,0 @@ -mutation setNewName($newName: String!, $sourceGroupId: String!) { - setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client { - id - import_target { - new_name - } - } -} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql deleted file mode 100644 index a0ef407f135..00000000000 --- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql +++ /dev/null @@ -1,8 +0,0 @@ -mutation setTargetNamespace($targetNamespace: String!, $sourceGroupId: String!) { - setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client { - id - import_target { - target_namespace - } - } -} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql similarity index 100% rename from app/assets/javascripts/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql rename to app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index 40eccefd7e0..ab42e8cdfeb 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -16,7 +16,12 @@ import Api from '~/api'; import ExperimentTracking from '~/experimentation/experiment_tracking'; import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import { s__, sprintf } from '~/locale'; -import { INVITE_MEMBERS_IN_COMMENT, GROUP_FILTERS, MEMBER_AREAS_OF_FOCUS } from '../constants'; +import { + INVITE_MEMBERS_IN_COMMENT, + GROUP_FILTERS, + USERS_FILTER_ALL, + MEMBER_AREAS_OF_FOCUS, +} from '../constants'; import eventHub from '../event_hub'; import { responseMessageFromError, @@ -72,6 +77,16 @@ export default { required: false, default: null, }, + usersFilter: { + type: String, + required: false, + default: USERS_FILTER_ALL, + }, + filterId: { + type: Number, + required: false, + default: null, + }, helpLink: { type: String, required: true, @@ -384,6 +399,8 @@ export default { class="gl-mb-2" :validation-state="validationState" :aria-labelledby="$options.membersTokenSelectLabelId" + :users-filter="usersFilter" + :filter-id="filterId" @clear="handleMembersTokenSelectClear" /> { this.users = response.data.map((token) => ({ id: token.id, @@ -98,7 +117,7 @@ export default { this.$emit('clear'); }, }, - queryOptions: { exclude_internal: true, active: true }, + defaultQueryOptions: { exclude_internal: true, active: true }, i18n: { inviteTextMessage: __('Invite "%{email}" by email'), }, diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index 01b35f2a656..d7daf83e26b 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -17,3 +17,5 @@ export const GROUP_FILTERS = { export const API_MESSAGES = { EMAIL_ALREADY_INVITED: __('Invite email has already been taken'), }; +export const USERS_FILTER_ALL = 'all'; +export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id'; diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js index db7e2ca4a71..c1dfaa25dc7 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -25,6 +25,8 @@ export default function initInviteMembersModal() { groupSelectParentId: parseInt(el.dataset.parentId, 10), areasOfFocusOptions: JSON.parse(el.dataset.areasOfFocusOptions), noSelectionAreasOfFocus: JSON.parse(el.dataset.noSelectionAreasOfFocus), + usersFilter: el.dataset.usersFilter, + filterId: parseInt(el.dataset.filterId, 10), }, }), }); diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue index c13bde6b41a..a307b7c0b8a 100644 --- a/app/assets/javascripts/repository/components/delete_blob_modal.vue +++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue @@ -8,6 +8,8 @@ import { COMMIT_LABEL, TARGET_BRANCH_LABEL, TOGGLE_CREATE_MR_LABEL, + COMMIT_MESSAGE_SUBJECT_MAX_LENGTH, + COMMIT_MESSAGE_BODY_MAX_LENGTH, } from '../constants'; const initFormField = ({ value, required = true, skipValidation = false }) => ({ @@ -122,19 +124,16 @@ export default { return this.form.fields['commit_message'].value && this.form.fields['branch_name'].value; }, showHint() { - const commitMessageSubjectMaxLength = 52; - const commitMessageBodyMaxLength = 72; - const splitCommitMessageByLineBreak = this.form.fields['commit_message'].value .trim() .split('\n'); const [firstLine, ...otherLines] = splitCommitMessageByLineBreak; - const hasFirstLineExceedMaxLength = firstLine.length > commitMessageSubjectMaxLength; + const hasFirstLineExceedMaxLength = firstLine.length > COMMIT_MESSAGE_SUBJECT_MAX_LENGTH; const hasOtherLineExceedMaxLength = Boolean(otherLines.length) && - otherLines.some((text) => text.length > commitMessageBodyMaxLength); + otherLines.some((text) => text.length > COMMIT_MESSAGE_BODY_MAX_LENGTH); return ( !this.form.fields['commit_message'].feedback && diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js index 2d2faa8d9f3..b536bcb1875 100644 --- a/app/assets/javascripts/repository/constants.js +++ b/app/assets/javascripts/repository/constants.js @@ -8,3 +8,6 @@ export const SECONDARY_OPTIONS_TEXT = __('Cancel'); export const COMMIT_LABEL = __('Commit message'); export const TARGET_BRANCH_LABEL = __('Target branch'); export const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes'); + +export const COMMIT_MESSAGE_SUBJECT_MAX_LENGTH = 52; +export const COMMIT_MESSAGE_BODY_MAX_LENGTH = 72; diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js index 44bb1bb1af8..ebe0138f046 100644 --- a/app/assets/javascripts/security_configuration/components/constants.js +++ b/app/assets/javascripts/security_configuration/components/constants.js @@ -157,7 +157,7 @@ export const securityFeatures = [ // This field will eventually come from the backend, the progress is // tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/331621 - canEnableByMergeRequest: window.gon.features?.secDependencyScanningUiEnable, + canEnableByMergeRequest: true, }, { name: CONTAINER_SCANNING_NAME, diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb index 3274ea15b8b..e9e5f8b3a0b 100644 --- a/app/helpers/invite_members_helper.rb +++ b/app/helpers/invite_members_helper.rb @@ -78,4 +78,9 @@ module InviteMembersHelper } ] end + + # Overridden in EE + def users_filter_data(group) + {} + end end diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml index 8801ad98b8c..760efb16d40 100644 --- a/app/views/groups/_invite_members_modal.html.haml +++ b/app/views/groups/_invite_members_modal.html.haml @@ -2,4 +2,5 @@ .js-invite-members-modal{ data: { is_project: 'false', access_levels: GroupMember.access_level_roles.to_json, - help_link: help_page_url('user/permissions') }.merge(group_select_data(group)).merge(common_invite_modal_dataset(group)) } + default_access_level: Gitlab::Access::GUEST, + help_link: help_page_url('user/permissions') }.merge(group_select_data(group)).merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) } diff --git a/app/views/projects/_invite_members_modal.html.haml b/app/views/projects/_invite_members_modal.html.haml index 16964d2154a..f2fffeb23c5 100644 --- a/app/views/projects/_invite_members_modal.html.haml +++ b/app/views/projects/_invite_members_modal.html.haml @@ -2,4 +2,4 @@ .js-invite-members-modal{ data: { is_project: 'true', access_levels: ProjectMember.access_level_roles.to_json, - help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)) } + help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) } diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 634a7099b25..883f1db8e09 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -97,7 +97,8 @@ Line breaks were added to examples for legibility: "cpu_s":17.50, "db_duration_s":0.08, "view_duration_s":2.39, - "duration_s":20.54 + "duration_s":20.54, + "pid": 81836 } ``` @@ -120,6 +121,7 @@ seconds: - `redis__duration_s`: Total time to retrieve data from a Redis instance - `redis__read_bytes`: Total bytes read from a Redis instance - `redis__write_bytes`: Total bytes written to a Redis instance +- `pid`: Process ID of the Puma worker User clone and fetch activity using HTTP transport appears in the log as `action: git_upload_pack`. @@ -190,7 +192,8 @@ Starting with GitLab 12.5, if an error occurs, an "cpu_s":17.50, "db_duration_s":0.08, "view_duration_s":2.39, - "duration_s":20.54 + "duration_s":20.54, + "pid": 81836, "exception.class": "NameError", "exception.message": "undefined local variable or method `adsf' for #", "exception.backtrace": [ diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index ed4ec1717ed..0eb4d77a783 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -346,17 +346,8 @@ always take the latest dependency scanning artifact available. ### Enable Dependency Scanning via an automatic merge request > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4908) in GitLab 14.1. -> - [Deployed behind a feature flag](../../../user/feature_flags.md), enabled by default. -> - Enabled on GitLab.com. -> - Recommended for production use. -> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-configure-dependency-scanning-via-a-merge-request). **(ULTIMATE SELF)** - -WARNING: -This feature might not be available to you. Check the **version history** note above for details. - -There can be -[risks when disabling released features](../../../administration/feature_flags.md#risks-when-disabling-released-features). -Refer to this feature's version history for more details. +> - [Enabled with `sec_dependency_scanning_ui_enable` flag](https://gitlab.com/gitlab-org/gitlab/-/issues/282533) for self-managed GitLab in GitLab 14.1 and is ready for production use. +> - [Feature flag sec_dependency_scanning_ui_enable removed](https://gitlab.com/gitlab-org/gitlab/-/issues/326005) in GitLab 14.2. To enable Dependency Scanning in a project, you can create a merge request from the Security Configuration page. @@ -906,22 +897,3 @@ with a dependency on this version of Python should use `retire.js` version 2.10. ### Error: `dependency_scanning is used for configuration only, and its script should not be executed` For information on this, see the [GitLab Secure troubleshooting section](../index.md#error-job-is-used-for-configuration-only-and-its-script-should-not-be-executed). - -### Enable or disable Configure Dependency Scanning via a Merge Request - -Configure Dependency Scanning via a Merge Request is under development but ready for production use. -It is deployed behind a feature flag that is **enabled by default**. -[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) -can opt to disable it. - -To disable it: - -```ruby -Feature.disable(:sec_dependency_scanning_ui_enable) -``` - -To enable it: - -```ruby -Feature.enable(:sec_dependency_scanning_ui_enable) -``` diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb index c10c4a7bedf..23acf1e8e86 100644 --- a/lib/gitlab/instrumentation_helper.rb +++ b/lib/gitlab/instrumentation_helper.rb @@ -30,6 +30,7 @@ module Gitlab instrument_cpu(payload) instrument_thread_memory_allocations(payload) instrument_load_balancing(payload) + instrument_pid(payload) end def instrument_gitaly(payload) @@ -99,6 +100,10 @@ module Gitlab payload[:cpu_s] = cpu_s.round(DURATION_PRECISION) if cpu_s end + def instrument_pid(payload) + payload[:pid] = Process.pid + end + def instrument_thread_memory_allocations(payload) counters = ::Gitlab::Memory::Instrumentation.measure_thread_memory_allocations( ::Gitlab::RequestContext.instance.thread_memory_allocations) diff --git a/qa/Gemfile b/qa/Gemfile index f025e66fbe3..3ba1244d17e 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -12,7 +12,7 @@ gem 'rspec', '~> 3.10' gem 'selenium-webdriver', '~> 4.0.0.beta4' gem 'airborne', '~> 0.3.4' gem 'rest-client', '~> 2.1.0' -gem 'nokogiri', '~> 1.11.1' +gem 'nokogiri', '~> 1.11.7' gem 'rspec-retry', '~> 0.6.1' gem 'rspec_junit_formatter', '~> 0.4.1' gem 'faker', '~> 1.6', '>= 1.6.6' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index dfa7a49f2ef..66b635868f8 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -101,11 +101,11 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2021.0704) mini_mime (1.1.0) - mini_portile2 (2.5.0) + mini_portile2 (2.5.3) minitest (5.14.4) multipart-post (2.1.1) netrc (0.11.0) - nokogiri (1.11.1) + nokogiri (1.11.7) mini_portile2 (~> 2.5.0) racc (~> 1.4) octokit (4.21.0) @@ -221,7 +221,7 @@ DEPENDENCIES faker (~> 1.6, >= 1.6.6) gitlab-qa knapsack (~> 1.17) - nokogiri (~> 1.11.1) + nokogiri (~> 1.11.7) octokit (~> 4.21) parallel (~> 1.19) parallel_tests (~> 2.29) diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js index c52fc395487..5b6794cbc66 100644 --- a/spec/frontend/content_editor/components/content_editor_spec.js +++ b/spec/frontend/content_editor/components/content_editor_spec.js @@ -6,6 +6,8 @@ import ContentEditor from '~/content_editor/components/content_editor.vue'; import TopToolbar from '~/content_editor/components/top_toolbar.vue'; import { createContentEditor } from '~/content_editor/services/create_content_editor'; +jest.mock('~/emoji'); + describe('ContentEditor', () => { let wrapper; let editor; diff --git a/spec/frontend/content_editor/extensions/emoji_spec.js b/spec/frontend/content_editor/extensions/emoji_spec.js new file mode 100644 index 00000000000..c1b8dc9bdbb --- /dev/null +++ b/spec/frontend/content_editor/extensions/emoji_spec.js @@ -0,0 +1,57 @@ +import { initEmojiMock } from 'helpers/emoji'; +import Emoji from '~/content_editor/extensions/emoji'; +import { createTestEditor, createDocBuilder } from '../test_utils'; + +describe('content_editor/extensions/emoji', () => { + let tiptapEditor; + let doc; + let p; + let emoji; + let eq; + + beforeEach(async () => { + await initEmojiMock(); + }); + + beforeEach(() => { + tiptapEditor = createTestEditor({ extensions: [Emoji] }); + ({ + builders: { doc, p, emoji }, + eq, + } = createDocBuilder({ + tiptapEditor, + names: { + loading: { nodeType: Emoji.name }, + }, + })); + }); + + describe('when typing a valid emoji input rule', () => { + it('inserts an emoji node', () => { + const { view } = tiptapEditor; + const { selection } = view.state; + const expectedDoc = doc( + p( + ' ', + emoji({ moji: '❤', name: 'heart', title: 'heavy black heart', unicodeVersion: '1.1' }), + ), + ); + // Triggers the event handler that input rules listen to + view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, ':heart:')); + + expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); + }); + }); + + describe('when typing a invalid emoji input rule', () => { + it('does not insert an emoji node', () => { + const { view } = tiptapEditor; + const { selection } = view.state; + const invalidEmoji = ':invalid:'; + const expectedDoc = doc(p()); + // Triggers the event handler that input rules listen to + view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, invalidEmoji)); + expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); + }); + }); +}); diff --git a/spec/frontend/content_editor/markdown_processing_spec.js b/spec/frontend/content_editor/markdown_processing_spec.js index 028cd6a8271..da3f6e64db8 100644 --- a/spec/frontend/content_editor/markdown_processing_spec.js +++ b/spec/frontend/content_editor/markdown_processing_spec.js @@ -1,6 +1,8 @@ import { createContentEditor } from '~/content_editor'; import { loadMarkdownApiExamples, loadMarkdownApiResult } from './markdown_processing_examples'; +jest.mock('~/emoji'); + describe('markdown processing', () => { // Ensure we generate same markdown that was provided to Markdown API. it.each(loadMarkdownApiExamples())( diff --git a/spec/frontend/content_editor/services/create_content_editor_spec.js b/spec/frontend/content_editor/services/create_content_editor_spec.js index b78e7f0862d..6b2f28b3306 100644 --- a/spec/frontend/content_editor/services/create_content_editor_spec.js +++ b/spec/frontend/content_editor/services/create_content_editor_spec.js @@ -2,7 +2,9 @@ import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '~/content_editor/constants import { createContentEditor } from '~/content_editor/services/create_content_editor'; import { createTestContentEditorExtension } from '../test_utils'; -describe('content_editor/services/create_editor', () => { +jest.mock('~/emoji'); + +describe('content_editor/services/create_content_editor', () => { let renderMarkdown; let editor; const uploadsPath = '/uploads'; diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml index 8d8c9a1d902..e106dcf4889 100644 --- a/spec/frontend/fixtures/api_markdown.yml +++ b/spec/frontend/fixtures/api_markdown.yml @@ -86,4 +86,5 @@ |--------|------------|----------| | cell | cell | cell | | cell | cell | cell | - +- name: emoji + markdown: ':sparkles: :heart: :100:' diff --git a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js index 6255d6aba1f..6e059d5bc2b 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js @@ -2,19 +2,13 @@ import { GlButton, GlDropdownItem, GlLink, GlFormInput } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue'; import { STATUSES } from '~/import_entities/constants'; import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue'; -import addValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql'; -import removeValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql'; -import groupAndProjectQuery from '~/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql'; import { availableNamespacesFixture } from '../graphql/fixtures'; Vue.use(VueApollo); -const { i18n: I18N } = ImportTableRow; - const getFakeGroup = (status) => ({ web_url: 'https://fake.host/', full_path: 'fake_group_1', @@ -28,13 +22,8 @@ const getFakeGroup = (status) => ({ progress: { status }, }); -const EXISTING_GROUP_TARGET_NAMESPACE = 'existing-group'; -const EXISTING_GROUP_PATH = 'existing-path'; -const EXISTING_PROJECT_PATH = 'existing-project-path'; - describe('import table row', () => { let wrapper; - let apolloProvider; let group; const findByText = (cmp, text) => { @@ -45,27 +34,7 @@ describe('import table row', () => { const findNamespaceDropdown = () => wrapper.find(ImportGroupDropdown); const createComponent = (props) => { - apolloProvider = createMockApollo([ - [ - groupAndProjectQuery, - ({ fullPath }) => { - const existingGroup = - fullPath === `${EXISTING_GROUP_TARGET_NAMESPACE}/${EXISTING_GROUP_PATH}` - ? { id: 1 } - : null; - - const existingProject = - fullPath === `${EXISTING_GROUP_TARGET_NAMESPACE}/${EXISTING_PROJECT_PATH}` - ? { id: 1 } - : null; - - return Promise.resolve({ data: { existingGroup, existingProject } }); - }, - ], - ]); - wrapper = shallowMount(ImportTableRow, { - apolloProvider, stubs: { ImportGroupDropdown }, propsData: { availableNamespaces: availableNamespacesFixture, @@ -224,101 +193,5 @@ describe('import table row', () => { expect(wrapper.text()).toContain(FAKE_ERROR_MESSAGE); }); - - it('sets validation error when targetting existing group', async () => { - const testGroup = getFakeGroup(STATUSES.NONE); - - createComponent({ - group: { - ...testGroup, - import_target: { - target_namespace: EXISTING_GROUP_TARGET_NAMESPACE, - new_name: EXISTING_GROUP_PATH, - }, - }, - }); - - jest.spyOn(wrapper.vm.$apollo, 'mutate'); - - jest.runOnlyPendingTimers(); - await nextTick(); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: addValidationErrorMutation, - variables: { - field: 'new_name', - message: I18N.NAME_ALREADY_EXISTS, - sourceGroupId: testGroup.id, - }, - }); - }); - - it('sets validation error when targetting existing project', async () => { - const testGroup = getFakeGroup(STATUSES.NONE); - - createComponent({ - group: { - ...testGroup, - import_target: { - target_namespace: EXISTING_GROUP_TARGET_NAMESPACE, - new_name: EXISTING_PROJECT_PATH, - }, - }, - }); - - jest.spyOn(wrapper.vm.$apollo, 'mutate'); - - jest.runOnlyPendingTimers(); - await nextTick(); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: addValidationErrorMutation, - variables: { - field: 'new_name', - message: I18N.NAME_ALREADY_EXISTS, - sourceGroupId: testGroup.id, - }, - }); - }); - - it('clears validation error when target is updated', async () => { - const testGroup = getFakeGroup(STATUSES.NONE); - - createComponent({ - group: { - ...testGroup, - import_target: { - target_namespace: EXISTING_GROUP_TARGET_NAMESPACE, - new_name: EXISTING_PROJECT_PATH, - }, - }, - }); - - jest.runOnlyPendingTimers(); - await nextTick(); - - jest.spyOn(wrapper.vm.$apollo, 'mutate'); - - await wrapper.setProps({ - group: { - ...testGroup, - import_target: { - target_namespace: 'valid_namespace', - new_name: 'valid_path', - }, - }, - }); - - jest.runOnlyPendingTimers(); - await nextTick(); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: removeValidationErrorMutation, - variables: { - field: 'new_name', - sourceGroupId: testGroup.id, - }, - }); - }); }); }); diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index 1c3e4d0b1e1..5344afe7aa1 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -16,8 +16,7 @@ import { STATUSES } from '~/import_entities/constants'; import ImportTable from '~/import_entities/import_groups/components/import_table.vue'; import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue'; import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql'; -import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql'; -import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql'; +import setImportTargetMutation from '~/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures'; @@ -140,10 +139,10 @@ describe('import table', () => { }); it.each` - event | payload | mutation | variables - ${'update-target-namespace'} | ${'new-namespace'} | ${setTargetNamespaceMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'new-namespace' }} - ${'update-new-name'} | ${'new-name'} | ${setNewNameMutation} | ${{ sourceGroupId: FAKE_GROUP.id, newName: 'new-name' }} - ${'import-group'} | ${undefined} | ${importGroupsMutation} | ${{ sourceGroupIds: [FAKE_GROUP.id] }} + event | payload | mutation | variables + ${'update-target-namespace'} | ${'new-namespace'} | ${setImportTargetMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'new-namespace', newName: 'group1' }} + ${'update-new-name'} | ${'new-name'} | ${setImportTargetMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'root', newName: 'new-name' }} + ${'import-group'} | ${undefined} | ${importGroupsMutation} | ${{ sourceGroupIds: [FAKE_GROUP.id] }} `('correctly maps $event to mutation', async ({ event, payload, mutation, variables }) => { jest.spyOn(apolloProvider.defaultClient, 'mutate'); wrapper.find(ImportTableRow).vm.$emit(event, payload); diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js index ef83c9ebbc4..ec50dfd037f 100644 --- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js +++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js @@ -12,12 +12,12 @@ import addValidationErrorMutation from '~/import_entities/import_groups/graphql/ import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql'; import removeValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql'; import setImportProgressMutation from '~/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql'; -import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql'; -import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql'; +import setImportTargetMutation from '~/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql'; import updateImportStatusMutation from '~/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql'; import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql'; import bulkImportSourceGroupQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql'; import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql'; +import groupAndProjectQuery from '~/import_entities/import_groups/graphql/queries/group_and_project.query.graphql'; import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller'; import axios from '~/lib/utils/axios_utils'; @@ -38,18 +38,29 @@ const FAKE_ENDPOINTS = { jobs: '/fake_jobs', }; +const FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER = jest.fn().mockResolvedValue({ + data: { + existingGroup: null, + existingProject: null, + }, +}); + describe('Bulk import resolvers', () => { let axiosMockAdapter; let client; const createClient = (extraResolverArgs) => { - return createMockClient({ + const mockedClient = createMockClient({ cache: new InMemoryCache({ fragmentMatcher: { match: () => true }, addTypename: false, }), resolvers: createResolvers({ endpoints: FAKE_ENDPOINTS, ...extraResolverArgs }), }); + + mockedClient.setRequestHandler(groupAndProjectQuery, FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER); + + return mockedClient; }; beforeEach(() => { @@ -196,6 +207,12 @@ describe('Bulk import resolvers', () => { const [statusPoller] = StatusPoller.mock.instances; expect(statusPoller.startPolling).toHaveBeenCalled(); }); + + it('requests validation status when request completes', async () => { + expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).not.toHaveBeenCalled(); + jest.runOnlyPendingTimers(); + expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).toHaveBeenCalled(); + }); }); it.each` @@ -256,40 +273,49 @@ describe('Bulk import resolvers', () => { }); }); - it('setTargetNamespaces updates group target namespace', async () => { - const NEW_TARGET_NAMESPACE = 'target'; - const { - data: { - setTargetNamespace: { - id: idInResponse, - import_target: { target_namespace: namespaceInResponse }, + describe('setImportTarget', () => { + it('updates group target namespace and name', async () => { + const NEW_TARGET_NAMESPACE = 'target'; + const NEW_NAME = 'new'; + + const { + data: { + setImportTarget: { + id: idInResponse, + import_target: { target_namespace: namespaceInResponse, new_name: newNameInResponse }, + }, }, - }, - } = await client.mutate({ - mutation: setTargetNamespaceMutation, - variables: { sourceGroupId: GROUP_ID, targetNamespace: NEW_TARGET_NAMESPACE }, + } = await client.mutate({ + mutation: setImportTargetMutation, + variables: { + sourceGroupId: GROUP_ID, + targetNamespace: NEW_TARGET_NAMESPACE, + newName: NEW_NAME, + }, + }); + + expect(idInResponse).toBe(GROUP_ID); + expect(namespaceInResponse).toBe(NEW_TARGET_NAMESPACE); + expect(newNameInResponse).toBe(NEW_NAME); }); - expect(idInResponse).toBe(GROUP_ID); - expect(namespaceInResponse).toBe(NEW_TARGET_NAMESPACE); - }); + it('invokes validation', async () => { + const NEW_TARGET_NAMESPACE = 'target'; + const NEW_NAME = 'new'; - it('setNewName updates group target name', async () => { - const NEW_NAME = 'new'; - const { - data: { - setNewName: { - id: idInResponse, - import_target: { new_name: nameInResponse }, + await client.mutate({ + mutation: setImportTargetMutation, + variables: { + sourceGroupId: GROUP_ID, + targetNamespace: NEW_TARGET_NAMESPACE, + newName: NEW_NAME, }, - }, - } = await client.mutate({ - mutation: setNewNameMutation, - variables: { sourceGroupId: GROUP_ID, newName: NEW_NAME }, - }); + }); - expect(idInResponse).toBe(GROUP_ID); - expect(nameInResponse).toBe(NEW_NAME); + expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).toHaveBeenCalledWith({ + fullPath: `${NEW_TARGET_NAMESPACE}/${NEW_NAME}`, + }); + }); }); describe('importGroup', () => { diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js index 12db7e42464..196a716d08c 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -12,11 +12,12 @@ const user1 = { id: 1, name: 'John Smith', username: 'one_1', avatar_url: '' }; const user2 = { id: 2, name: 'Jane Doe', username: 'two_2', avatar_url: '' }; const allUsers = [user1, user2]; -const createComponent = () => { +const createComponent = (props) => { return shallowMount(MembersTokenSelect, { propsData: { ariaLabelledby: label, placeholder, + ...props, }, stubs: { GlTokenSelector: stubComponent(GlTokenSelector), @@ -27,11 +28,6 @@ const createComponent = () => { describe('MembersTokenSelect', () => { let wrapper; - beforeEach(() => { - jest.spyOn(UserApi, 'getUsers').mockResolvedValue({ data: allUsers }); - wrapper = createComponent(); - }); - afterEach(() => { wrapper.destroy(); wrapper = null; @@ -41,6 +37,8 @@ describe('MembersTokenSelect', () => { describe('rendering the token-selector component', () => { it('renders with the correct props', () => { + wrapper = createComponent(); + const expectedProps = { ariaLabelledby: label, placeholder, @@ -51,6 +49,11 @@ describe('MembersTokenSelect', () => { }); describe('users', () => { + beforeEach(() => { + jest.spyOn(UserApi, 'getUsers').mockResolvedValue({ data: allUsers }); + wrapper = createComponent(); + }); + describe('when input is focused for the first time (modal auto-focus)', () => { it('does not call the API', async () => { findTokenSelector().vm.$emit('focus'); @@ -90,10 +93,10 @@ describe('MembersTokenSelect', () => { await waitForPromises(); - expect(UserApi.getUsers).toHaveBeenCalledWith( - searchParam, - wrapper.vm.$options.queryOptions, - ); + expect(UserApi.getUsers).toHaveBeenCalledWith(searchParam, { + active: true, + exclude_internal: true, + }); expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false); }); @@ -134,6 +137,8 @@ describe('MembersTokenSelect', () => { describe('when text input is blurred', () => { it('clears text input', async () => { + wrapper = createComponent(); + const tokenSelector = findTokenSelector(); tokenSelector.vm.$emit('blur'); @@ -143,4 +148,33 @@ describe('MembersTokenSelect', () => { expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false); }); }); + + describe('when component is mounted for a group using a saml provider', () => { + const searchParam = 'name'; + const samlProviderId = 123; + let resolveApiRequest; + + beforeEach(() => { + jest.spyOn(UserApi, 'getUsers').mockImplementation( + () => + new Promise((resolve) => { + resolveApiRequest = resolve; + }), + ); + + wrapper = createComponent({ filterId: samlProviderId, usersFilter: 'saml_provider_id' }); + + findTokenSelector().vm.$emit('text-input', searchParam); + }); + + it('calls the API with the saml provider ID param', () => { + resolveApiRequest({ data: allUsers }); + + expect(UserApi.getUsers).toHaveBeenCalledWith(searchParam, { + active: true, + exclude_internal: true, + saml_provider_id: samlProviderId, + }); + }); + }); }); diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js index f36d6262b5f..97cb70dbd62 100644 --- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js +++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js @@ -15,6 +15,8 @@ import { import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +jest.mock('~/emoji'); + describe('WikiForm', () => { let wrapper; let mock; diff --git a/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb index 95e3af34174..641fb27a071 100644 --- a/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb +++ b/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb @@ -3,26 +3,23 @@ require 'spec_helper' RSpec.describe Gitlab::GrapeLogging::Loggers::PerfLogger do - subject { described_class.new } + let(:mock_request) { OpenStruct.new(env: {}) } describe ".parameters" do - let(:mock_request) { OpenStruct.new(env: {}) } + subject { described_class.new.parameters(mock_request, nil) } - describe 'when no performance datais are present' do - it 'returns an empty Hash' do - expect(subject.parameters(mock_request, nil)).to eq({}) - end + let(:perf_data) { { redis_calls: 1 } } + + describe 'when no performance data present' do + it { is_expected.not_to include(perf_data) } end - describe 'when Redis calls are present', :request_store do - it 'returns a Hash with Redis information' do + describe 'when performance data present', :request_store do + before do Gitlab::Redis::SharedState.with { |redis| redis.get('perf-logger-test') } - - payload = subject.parameters(mock_request, nil) - - expect(payload[:redis_calls]).to eq(1) - expect(payload[:redis_duration_s]).to be >= 0 end + + it { is_expected.to include(perf_data) } end end end diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index 0f670f5861c..85daf50717c 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -83,6 +83,12 @@ RSpec.describe Gitlab::InstrumentationHelper do expect(payload).to include(:cpu_s) end + it 'logs the process ID' do + subject + + expect(payload).to include(:pid) + end + context 'when logging memory allocations' do include MemoryInstrumentationHelper