diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-03 00:11:20 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-03 00:11:20 +0000 |
commit | 498ba9dc41fcf2b4be30a8f3721543953efb3c3b (patch) | |
tree | ed33fbf37c0c2ae3a71042455f9b51800907a984 | |
parent | 515f39456fce82eb2ab811fa366167ad084a3b12 (diff) | |
download | gitlab-ce-498ba9dc41fcf2b4be30a8f3721543953efb3c3b.tar.gz |
Add latest changes from gitlab-org/gitlab@master
49 files changed, 694 insertions, 510 deletions
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue index a575b80facc..67e3998bc97 100644 --- a/app/assets/javascripts/header_search/components/app.vue +++ b/app/assets/javascripts/header_search/components/app.vue @@ -2,9 +2,14 @@ import { GlSearchBoxByType, GlOutsideDirective as Outside } from '@gitlab/ui'; import { mapState, mapActions, mapGetters } from 'vuex'; import { visitUrl } from '~/lib/utils/url_utility'; -import { __ } from '~/locale'; +import { s__, sprintf } from '~/locale'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; -import { FIRST_DROPDOWN_INDEX, SEARCH_BOX_INDEX } from '../constants'; +import { + FIRST_DROPDOWN_INDEX, + SEARCH_BOX_INDEX, + SEARCH_INPUT_DESCRIPTION, + SEARCH_RESULTS_DESCRIPTION, +} from '../constants'; import HeaderSearchAutocompleteItems from './header_search_autocomplete_items.vue'; import HeaderSearchDefaultItems from './header_search_default_items.vue'; import HeaderSearchScopedItems from './header_search_scoped_items.vue'; @@ -12,7 +17,21 @@ import HeaderSearchScopedItems from './header_search_scoped_items.vue'; export default { name: 'HeaderSearchApp', i18n: { - searchPlaceholder: __('Search or jump to...'), + searchPlaceholder: s__('GlobalSearch|Search or jump to...'), + searchAria: s__('GlobalSearch|Search GitLab'), + searchInputDescribeByNoDropdown: s__( + 'GlobalSearch|Type and press the enter key to submit search.', + ), + searchInputDescribeByWithDropdown: s__( + 'GlobalSearch|Type for new suggestions to appear below.', + ), + searchDescribedByDefault: s__( + 'GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list.', + ), + searchDescribedByUpdated: s__( + 'GlobalSearch|Results updated. %{count} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.', + ), + searchResultsLoading: s__('GlobalSearch|Search results are loading'), }, directives: { Outside }, components: { @@ -29,7 +48,7 @@ export default { }; }, computed: { - ...mapState(['search']), + ...mapState(['search', 'loading']), ...mapGetters(['searchQuery', 'searchOptions']), searchText: { get() { @@ -42,6 +61,9 @@ export default { currentFocusedOption() { return this.searchOptions[this.currentFocusIndex]; }, + currentFocusedId() { + return this.currentFocusedOption?.html_id; + }, isLoggedIn() { return gon?.current_username; }, @@ -58,6 +80,30 @@ export default { return FIRST_DROPDOWN_INDEX; }, + searchInputDescribeBy() { + if (this.isLoggedIn) { + return this.$options.i18n.searchInputDescribeByWithDropdown; + } + + return this.$options.i18n.searchInputDescribeByNoDropdown; + }, + dropdownResultsDescription() { + if (!this.showSearchDropdown) { + return ''; // This allows aria-live to see register an update when the dropdown is shown + } + + if (this.showDefaultItems) { + return sprintf(this.$options.i18n.searchDescribedByDefault, { + count: this.searchOptions.length, + }); + } + + return this.loading + ? this.$options.i18n.searchResultsLoading + : sprintf(this.$options.i18n.searchDescribedByUpdated, { + count: this.searchOptions.length, + }); + }, }, methods: { ...mapActions(['setSearch', 'fetchAutocompleteOptions']), @@ -79,22 +125,44 @@ export default { }, }, SEARCH_BOX_INDEX, + SEARCH_INPUT_DESCRIPTION, + SEARCH_RESULTS_DESCRIPTION, }; </script> <template> - <section v-outside="closeDropdown" class="header-search gl-relative"> + <form + v-outside="closeDropdown" + role="search" + :aria-label="$options.i18n.searchAria" + class="header-search gl-relative" + > <gl-search-box-by-type v-model="searchText" + role="searchbox" class="gl-z-index-1" :debounce="500" autocomplete="off" :placeholder="$options.i18n.searchPlaceholder" + :aria-activedescendant="currentFocusedId" + :aria-describedby="$options.SEARCH_INPUT_DESCRIPTION" @focus="openDropdown" @click="openDropdown" @input="getAutocompleteOptions" @keydown.enter.stop.prevent="submitSearch" /> + <span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only">{{ + searchInputDescribeBy + }}</span> + <span + role="region" + :data-testid="$options.SEARCH_RESULTS_DESCRIPTION" + class="gl-sr-only" + aria-live="polite" + aria-atomic="true" + > + {{ dropdownResultsDescription }} + </span> <div v-if="showSearchDropdown" data-testid="header-search-dropdown-menu" @@ -118,5 +186,5 @@ export default { </template> </div> </div> - </section> + </form> </template> diff --git a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue index cf1f7c030e2..9f4f4768247 100644 --- a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue +++ b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue @@ -69,13 +69,16 @@ export default { <gl-dropdown-section-header>{{ option.category }}</gl-dropdown-section-header> <gl-dropdown-item v-for="data in option.data" + :id="data.html_id" :ref="data.html_id" :key="data.html_id" :class="{ 'gl-bg-gray-50': isOptionFocused(data) }" + :aria-selected="isOptionFocused(data)" + :aria-label="data.label" tabindex="-1" :href="data.url" > - <div class="gl-display-flex gl-align-items-center"> + <div class="gl-display-flex gl-align-items-center" aria-hidden="true"> <gl-avatar v-if="data.avatar_url !== undefined" :src="data.avatar_url" diff --git a/app/assets/javascripts/header_search/components/header_search_default_items.vue b/app/assets/javascripts/header_search/components/header_search_default_items.vue index 2228bc0c2a2..53e63bc6cca 100644 --- a/app/assets/javascripts/header_search/components/header_search_default_items.vue +++ b/app/assets/javascripts/header_search/components/header_search_default_items.vue @@ -43,13 +43,16 @@ export default { <gl-dropdown-section-header>{{ sectionHeader }}</gl-dropdown-section-header> <gl-dropdown-item v-for="option in defaultSearchOptions" + :id="option.html_id" :ref="option.html_id" :key="option.html_id" :class="{ 'gl-bg-gray-50': isOptionFocused(option) }" + :aria-selected="isOptionFocused(option)" + :aria-label="option.title" tabindex="-1" :href="option.url" > - {{ option.title }} + <span aria-hidden="true">{{ option.title }}</span> </gl-dropdown-item> </div> </template> diff --git a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue index d3def929752..3aebee71509 100644 --- a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue +++ b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue @@ -1,6 +1,7 @@ <script> import { GlDropdownItem } from '@gitlab/ui'; import { mapState, mapGetters } from 'vuex'; +import { __, sprintf } from '~/locale'; export default { name: 'HeaderSearchScopedItems', @@ -22,6 +23,13 @@ export default { isOptionFocused(option) { return this.currentFocusedOption?.html_id === option.html_id; }, + ariaLabel(option) { + return sprintf(__('%{search} %{description} %{scope}'), { + search: this.search, + description: option.description, + scope: option.scope || '', + }); + }, }, }; </script> @@ -30,15 +38,20 @@ export default { <div> <gl-dropdown-item v-for="option in scopedSearchOptions" + :id="option.html_id" :ref="option.html_id" :key="option.html_id" :class="{ 'gl-bg-gray-50': isOptionFocused(option) }" + :aria-selected="isOptionFocused(option)" + :aria-label="ariaLabel(option)" tabindex="-1" :href="option.url" > - "<span class="gl-font-weight-bold">{{ search }}</span - >" {{ option.description }} - <span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span> + <span aria-hidden="true"> + "<span class="gl-font-weight-bold">{{ search }}</span + >" {{ option.description }} + <span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span> + </span> </gl-dropdown-item> </div> </template> diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js index 34777697863..b2e45fcd648 100644 --- a/app/assets/javascripts/header_search/constants.js +++ b/app/assets/javascripts/header_search/constants.js @@ -1,20 +1,20 @@ -import { __ } from '~/locale'; +import { s__ } from '~/locale'; -export const MSG_ISSUES_ASSIGNED_TO_ME = __('Issues assigned to me'); +export const MSG_ISSUES_ASSIGNED_TO_ME = s__('GlobalSearch|Issues assigned to me'); -export const MSG_ISSUES_IVE_CREATED = __("Issues I've created"); +export const MSG_ISSUES_IVE_CREATED = s__("GlobalSearch|Issues I've created"); -export const MSG_MR_ASSIGNED_TO_ME = __('Merge requests assigned to me'); +export const MSG_MR_ASSIGNED_TO_ME = s__('GlobalSearch|Merge requests assigned to me'); -export const MSG_MR_IM_REVIEWER = __("Merge requests that I'm a reviewer"); +export const MSG_MR_IM_REVIEWER = s__("GlobalSearch|Merge requests that I'm a reviewer"); -export const MSG_MR_IVE_CREATED = __("Merge requests I've created"); +export const MSG_MR_IVE_CREATED = s__("GlobalSearch|Merge requests I've created"); -export const MSG_IN_ALL_GITLAB = __('in all GitLab'); +export const MSG_IN_ALL_GITLAB = s__('GlobalSearch|in all GitLab'); -export const MSG_IN_GROUP = __('in group'); +export const MSG_IN_GROUP = s__('GlobalSearch|in group'); -export const MSG_IN_PROJECT = __('in project'); +export const MSG_IN_PROJECT = s__('GlobalSearch|in project'); export const GROUPS_CATEGORY = 'Groups'; @@ -27,3 +27,7 @@ export const SMALL_AVATAR_PX = 16; export const FIRST_DROPDOWN_INDEX = 0; export const SEARCH_BOX_INDEX = -1; + +export const SEARCH_INPUT_DESCRIPTION = 'search-input-description'; + +export const SEARCH_RESULTS_DESCRIPTION = 'search-results-description'; 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 a6e747c6b48..7163d1be773 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -23,7 +23,6 @@ import { INVITE_MEMBERS_IN_COMMENT, GROUP_FILTERS, USERS_FILTER_ALL, - MEMBER_AREAS_OF_FOCUS, INVITE_MEMBERS_FOR_TASK, MODAL_LABELS, LEARN_GITLAB, @@ -101,14 +100,6 @@ export default { type: String, required: true, }, - areasOfFocusOptions: { - type: Array, - required: true, - }, - noSelectionAreasOfFocus: { - type: Array, - required: true, - }, tasksToBeDoneOptions: { type: Array, required: true, @@ -126,7 +117,6 @@ export default { inviteeType: 'members', newUsersToInvite: [], selectedDate: undefined, - selectedAreasOfFocus: [], selectedTasksToBeDone: [], selectedTaskProject: this.projects[0], groupToBeSharedWith: {}, @@ -182,16 +172,6 @@ export default { this.newUsersToInvite.length === 0 && Object.keys(this.groupToBeSharedWith).length === 0 ); }, - areasOfFocusEnabled() { - return !this.tasksToBeDoneEnabled && this.areasOfFocusOptions.length !== 0; - }, - areasOfFocusForPost() { - if (this.selectedAreasOfFocus.length === 0 && this.areasOfFocusEnabled) { - return this.noSelectionAreasOfFocus; - } - - return this.selectedAreasOfFocus; - }, errorFieldDescription() { if (this.inviteeType === 'group') { return ''; @@ -232,8 +212,6 @@ export default { this.openModal(options); if (this.isOnLearnGitlab) { this.trackEvent(INVITE_MEMBERS_FOR_TASK.name, this.source); - } else { - this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.view); } }); @@ -280,8 +258,6 @@ export default { if (this.source === INVITE_MEMBERS_IN_COMMENT) { this.trackEvent(INVITE_MEMBERS_IN_COMMENT, 'comment_invite_success'); } - - this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.submit); }, trackinviteMembersForTask() { const label = 'selected_tasks_to_be_done'; @@ -296,7 +272,6 @@ export default { this.newUsersToInvite = []; this.groupToBeSharedWith = {}; this.invalidFeedbackMessage = ''; - this.selectedAreasOfFocus = []; this.selectedTasksToBeDone = []; [this.selectedTaskProject] = this.projects; }, @@ -350,7 +325,6 @@ export default { email: usersToInviteByEmail, access_level: this.selectedAccessLevel, invite_source: this.source, - areas_of_focus: this.areasOfFocusForPost, tasks_to_be_done: this.tasksToBeDoneForPost, tasks_project_id: this.tasksProjectForPost, }; @@ -361,7 +335,6 @@ export default { user_id: usersToAddById, access_level: this.selectedAccessLevel, invite_source: this.source, - areas_of_focus: this.areasOfFocusForPost, tasks_to_be_done: this.tasksToBeDoneForPost, tasks_project_id: this.tasksProjectForPost, }; @@ -517,16 +490,6 @@ export default { </template> </gl-datepicker> </div> - <div v-if="areasOfFocusEnabled"> - <label class="gl-mt-5"> - {{ $options.labels.areasOfFocusLabel }} - </label> - <gl-form-checkbox-group - v-model="selectedAreasOfFocus" - :options="areasOfFocusOptions" - data-testid="area-of-focus-checks" - /> - </div> <div v-if="showTasksToBeDone" data-testid="invite-members-modal-tasks-to-be-done"> <label class="gl-mt-5"> {{ $options.labels.members.tasksToBeDone.title }} diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index 2a4e7041ed1..87d2fbc6aac 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -3,11 +3,6 @@ import { __, s__ } from '~/locale'; export const SEARCH_DELAY = 200; export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment'; -export const MEMBER_AREAS_OF_FOCUS = { - name: 'member_areas_of_focus', - view: 'view', - submit: 'submit', -}; export const INVITE_MEMBERS_FOR_TASK = { minimum_access_level: 30, name: 'invite_members_for_task', @@ -77,9 +72,6 @@ export const READ_MORE_TEXT = s__( export const INVITE_BUTTON_TEXT = s__('InviteMembersModal|Invite'); export const CANCEL_BUTTON_TEXT = s__('InviteMembersModal|Cancel'); export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team members'); -export const AREAS_OF_FOCUS_LABEL = s__( - 'InviteMembersModal|What would you like new member(s) to focus on? (optional)', -); export const MODAL_LABELS = { members: { @@ -142,7 +134,6 @@ export const MODAL_LABELS = { inviteButtonText: INVITE_BUTTON_TEXT, cancelButtonText: CANCEL_BUTTON_TEXT, headerCloseLabel: HEADER_CLOSE_LABEL, - areasOfFocusLabel: AREAS_OF_FOCUS_LABEL, }; export const LEARN_GITLAB = 'learn_gitlab'; 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 fc657a064dd..2cc056f2ddb 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -40,10 +40,8 @@ export default function initInviteMembersModal() { defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10), groupSelectFilter: el.dataset.groupsFilter, groupSelectParentId: parseInt(el.dataset.parentId, 10), - areasOfFocusOptions: JSON.parse(el.dataset.areasOfFocusOptions), tasksToBeDoneOptions: JSON.parse(el.dataset.tasksToBeDoneOptions || '[]'), projects: JSON.parse(el.dataset.projects || '[]'), - noSelectionAreasOfFocus: JSON.parse(el.dataset.noSelectionAreasOfFocus), usersFilter: el.dataset.usersFilter, filterId: parseInt(el.dataset.filterId, 10), }, diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue index cd2add6407f..62c30c941eb 100644 --- a/app/assets/javascripts/security_configuration/components/app.vue +++ b/app/assets/javascripts/security_configuration/components/app.vue @@ -8,6 +8,7 @@ import AutoDevOpsAlert from './auto_dev_ops_alert.vue'; import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue'; import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants'; import FeatureCard from './feature_card.vue'; +import TrainingProviderList from './training_provider_list.vue'; import SectionLayout from './section_layout.vue'; import UpgradeBanner from './upgrade_banner.vue'; @@ -28,8 +29,28 @@ export const i18n = { securityTraining: s__('SecurityConfiguration|Security training'), }; +// This will be removed and replaced with GraphQL query: +// https://gitlab.com/gitlab-org/gitlab/-/issues/346480 +export const TRAINING_PROVIDERS = [ + { + id: 101, + name: __('Kontra'), + description: __('Interactive developer security education.'), + url: 'https://application.security/', + isEnabled: false, + }, + { + id: 102, + name: __('SecureCodeWarrior'), + description: __('Security training with guide and learning pathways.'), + url: 'https://www.securecodewarrior.com/', + isEnabled: true, + }, +]; + export default { i18n, + TRAINING_PROVIDERS, components: { AutoDevOpsAlert, AutoDevOpsEnabledAlert, @@ -43,6 +64,7 @@ export default { SectionLayout, UpgradeBanner, UserCalloutDismisser, + TrainingProviderList, }, mixins: [glFeatureFlagsMixin()], inject: ['projectPath'], @@ -240,7 +262,11 @@ export default { data-testid="vulnerability-management-tab" :title="$options.i18n.vulnerabilityManagement" > - <section-layout :heading="$options.i18n.securityTraining" /> + <section-layout :heading="$options.i18n.securityTraining"> + <template #features> + <training-provider-list :providers="$options.TRAINING_PROVIDERS" /> + </template> + </section-layout> </gl-tab> </gl-tabs> </article> diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue new file mode 100644 index 00000000000..160540f6989 --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue @@ -0,0 +1,36 @@ +<script> +import { GlCard, GlToggle, GlLink } from '@gitlab/ui'; + +export default { + components: { + GlCard, + GlToggle, + GlLink, + }, + props: { + providers: { + type: Array, + required: true, + }, + }, +}; +</script> + +<template> + <ul class="gl-list-style-none gl-m-0 gl-p-0"> + <li v-for="{ id, isEnabled, name, description, url } in providers" :key="id" class="gl-mb-6"> + <gl-card> + <div class="gl-display-flex"> + <gl-toggle :value="isEnabled" :label="__('Training mode')" label-position="hidden" /> + <div class="gl-ml-5"> + <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3> + <p> + {{ description }} + <gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link> + </p> + </div> + </div> + </gl-card> + </li> + </ul> +</template> diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 6725e19df25..dd30d688fa8 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -3,6 +3,7 @@ class ConfirmationsController < Devise::ConfirmationsController include AcceptsPendingInvitations include GitlabRecaptcha + include OneTrustCSP prepend_before_action :check_recaptcha, only: :create before_action :load_recaptcha, only: :new diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb index b3adda8c633..c002c9b83f9 100644 --- a/app/controllers/repositories/git_http_client_controller.rb +++ b/app/controllers/repositories/git_http_client_controller.rb @@ -8,12 +8,9 @@ module Repositories attr_reader :authentication_result, :redirected_path - delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true + delegate :authentication_abilities, to: :authentication_result, allow_nil: true delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result - alias_method :user, :actor - alias_method :authenticated_user, :actor - # Git clients will not know what authenticity token to send along skip_around_action :set_session_storage skip_before_action :verify_authenticity_token @@ -22,8 +19,16 @@ module Repositories feature_category :source_code_management + def authenticated_user + authentication_result&.user || authentication_result&.deploy_token + end + private + def user + authenticated_user + end + def download_request? raise NotImplementedError end diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb index 25d605ca8ee..8b26b646fdd 100644 --- a/app/helpers/invite_members_helper.rb +++ b/app/helpers/invite_members_helper.rb @@ -35,13 +35,6 @@ module InviteMembersHelper default_access_level: Gitlab::Access::GUEST } - experiment(:member_areas_of_focus, user: current_user) do |e| - e.publish_to_database - - e.control { dataset.merge!(areas_of_focus_options: [], no_selection_areas_of_focus: []) } - e.candidate { dataset.merge!(areas_of_focus_options: member_areas_of_focus_options.to_json, no_selection_areas_of_focus: ['no_selection']) } - end - if show_invite_members_for_task?(source) dataset.merge!( tasks_to_be_done_options: tasks_to_be_done_options.to_json, @@ -55,26 +48,6 @@ module InviteMembersHelper private - def member_areas_of_focus_options - [ - { - value: 'Contribute to the codebase', text: s_('InviteMembersModal|Contribute to the codebase') - }, - { - value: 'Collaborate on open issues and merge requests', text: s_('InviteMembersModal|Collaborate on open issues and merge requests') - }, - { - value: 'Configure CI/CD', text: s_('InviteMembersModal|Configure CI/CD') - }, - { - value: 'Configure security features', text: s_('InviteMembersModal|Configure security features') - }, - { - value: 'Other', text: s_('InviteMembersModal|Other') - } - ] - end - # Overridden in EE def users_filter_data(group) {} diff --git a/app/services/ci/play_build_service.rb b/app/services/ci/play_build_service.rb index c1cf06a4631..e2673c763f3 100644 --- a/app/services/ci/play_build_service.rb +++ b/app/services/ci/play_build_service.rb @@ -9,7 +9,7 @@ module Ci # if build.enqueue build.tap do |build| - build.update(user: current_user, job_variables_attributes: job_variables_attributes || []) + build.update!(user: current_user, job_variables_attributes: job_variables_attributes || []) AfterRequeueJobService.new(project, current_user).execute(build) end diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb index d9ee560c445..acd00d0d1ec 100644 --- a/app/services/members/create_service.rb +++ b/app/services/members/create_service.rb @@ -92,7 +92,6 @@ module Members super track_invite_source(member) - track_areas_of_focus(member) end def track_invite_source(member) @@ -110,12 +109,6 @@ module Members member.invite? ? 'net_new_user' : 'existing_user' end - def track_areas_of_focus(member) - areas_of_focus.each do |area_of_focus| - Gitlab::Tracking.event(self.class.name, 'area_of_focus', label: area_of_focus, property: member.id.to_s) - end - end - def create_tasks_to_be_done return if params[:tasks_to_be_done].blank? || params[:tasks_project_id].blank? @@ -128,10 +121,6 @@ module Members TasksToBeDone::CreateWorker.perform_async(member_task.id, current_user.id, valid_members.map(&:user_id)) end - def areas_of_focus - params[:areas_of_focus] || [] - end - def user_limit limit = params.fetch(:limit, DEFAULT_INVITE_LIMIT) diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml index 9fb0fb734f9..892ef730884 100644 --- a/app/views/devise/confirmations/almost_there.haml +++ b/app/views/devise/confirmations/almost_there.haml @@ -1,6 +1,8 @@ - user_email = "(#{params[:email]})" if params[:email].present? - request_link_start = '<a href="%{new_user_confirmation_path}">'.html_safe % { new_user_confirmation_path: new_user_confirmation_path } - request_link_end = '</a>'.html_safe +- content_for :page_specific_javascripts do + = render "layouts/one_trust" .well-confirmation.gl-text-center.gl-mb-6 %h1.gl-mt-0 diff --git a/config/feature_flags/experiment/member_areas_of_focus.yml b/config/feature_flags/experiment/member_areas_of_focus.yml deleted file mode 100644 index e728ee7e3d3..00000000000 --- a/config/feature_flags/experiment/member_areas_of_focus.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: member_areas_of_focus -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65273 -rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/406 -milestone: '14.2' -type: experiment -group: group::expansion -default_enabled: false diff --git a/config/initializers/wikicloth_patch.rb b/config/initializers/wikicloth_patch.rb new file mode 100644 index 00000000000..c033d9ad7ca --- /dev/null +++ b/config/initializers/wikicloth_patch.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require 'wikicloth' +require 'wikicloth/wiki_buffer/var' + +# Adds patch for changes in this PR: https://github.com/nricciar/wikicloth/pull/112/files +# +# That fix has already been merged, but the maintainers are not releasing new versions, so we +# need to patch it here. +# +# If they ever do release a version, then we can remove this file. +# +# See: https://gitlab.com/gitlab-org/gitlab/-/issues/334056#note_745336618 + +# Guard to ensure we remember to delete this patch if they ever release a new version of wikicloth +raise 'New version of WikiCloth detected, please remove this patch' unless Gem::Version.new(WikiCloth::VERSION) == Gem::Version.new('0.8.1') + +# rubocop:disable Style/ClassAndModuleChildren +# rubocop:disable Layout/SpaceAroundEqualsInParameterDefault +# rubocop:disable Style/HashSyntax +# rubocop:disable Layout/SpaceAfterComma +# rubocop:disable Style/RescueStandardError +# rubocop:disable Rails/Output +# rubocop:disable Style/MethodCallWithoutArgsParentheses +# rubocop:disable Layout/EmptyLinesAroundClassBody +# rubocop:disable Metrics/AbcSize +# rubocop:disable Metrics/CyclomaticComplexity +# rubocop:disable Metrics/PerceivedComplexity +# rubocop:disable Cop/LineBreakAroundConditionalBlock +# rubocop:disable Layout/EmptyLineAfterGuardClause +# rubocop:disable Performance/ReverseEach +# rubocop:disable Style/BlockDelimiters +# rubocop:disable Cop/LineBreakAroundConditionalBlock +# rubocop:disable Layout/MultilineBlockLayout +# rubocop:disable Layout/BlockEndNewline +module WikiCloth + class WikiCloth + def render(opt={}) + self.options = { :noedit => false, :locale => I18n.default_locale, :fast => true, :output => :html, :link_handler => self.link_handler, + :params => self.params, :sections => self.sections }.merge(self.options).merge(opt) + self.options[:link_handler].params = options[:params] + + I18n.locale = self.options[:locale] + + data = self.sections.collect { |s| s.render(self.options) }.join + + # This is the first patched line from: + # https://github.com/nricciar/wikicloth/pull/112/files#diff-eed3de11b953105f9181a6859d58f52af8912d28525fd2a289f8be184e66f531R69 + data.gsub!(/<!--.*?-->/m,"") + + data << "\n" if data.last(1) != "\n" + data << "garbage" + + buffer = WikiBuffer.new("",options) + + begin + if self.options[:fast] + until data.empty? + case data + when /\A\w+/ + data = $' + @current_row += $&.length + buffer.add_word($&) + when /\A[^\w]+(\w|)/m + data = $' + $&.each_char { |c| add_current_char(buffer,c) } + end + end + else + data.each_char { |c| add_current_char(buffer,c) } + end + rescue => err + debug_tree = buffer.buffers.collect { |b| b.debug }.join("-->") + puts I18n.t("unknown error on line", :line => @current_line, :row => @current_row, :tree => debug_tree) + raise err + end + + buffer.eof() + buffer.send("to_#{self.options[:output]}") + end + + end + + class WikiBuffer::Var < WikiBuffer + def to_html + return "" if will_not_be_rendered + + if self.is_function? + if Extension.function_exists?(function_name) + return Extension.functions[function_name][:klass].new(@options).instance_exec( params.collect { |p| p.strip }, &Extension.functions[function_name][:block] ).to_s + end + ret = default_functions(function_name,params.collect { |p| p.strip }) + ret ||= @options[:link_handler].function(function_name, params.collect { |p| p.strip }) + ret.to_s + elsif self.is_param? + ret = nil + @options[:buffer].buffers.reverse.each do |b| + ret = b.get_param(params[0],params[1]) if b.instance_of?(WikiBuffer::HTMLElement) && b.element_name == "template" + break unless ret.nil? + end + ret.to_s + else + # put template at beginning of buffer + template_stack = @options[:buffer].buffers.collect { |b| b.get_param("__name") if b.instance_of?(WikiBuffer::HTMLElement) && + b.element_name == "template" }.compact + if template_stack.last == params[0] + debug_tree = @options[:buffer].buffers.collect { |b| b.debug }.join("-->") + "<span class=\"error\">#{I18n.t('template loop detected', :tree => debug_tree)}</span>" + else + key = params[0].to_s.strip + key_options = params[1..-1].collect { |p| p.is_a?(Hash) ? { :name => p[:name].strip, :value => p[:value].strip } : p.strip } + key_options ||= [] + key_digest = Digest::MD5.hexdigest(key_options.to_a.sort {|x,y| (x.is_a?(Hash) ? x[:name] : x) <=> (y.is_a?(Hash) ? y[:name] : y) }.inspect) + + return @options[:params][key] if @options[:params].has_key?(key) + # if we have a valid cache fragment use it + return @options[:cache][key][key_digest] unless @options[:cache].nil? || @options[:cache][key].nil? || @options[:cache][key][key_digest].nil? + + ret = @options[:link_handler].include_resource(key,key_options).to_s + + # This is the second patched line from: + # https://github.com/nricciar/wikicloth/pull/112/files#diff-f262faf4fadb222cca87185be0fb65b3f49659abc840794cc83a736d41310fb1R83 + ret.gsub!(/<!--.*?-->/m,"") unless ret.frozen? + + count = 0 + tag_attr = key_options.collect { |p| + if p.instance_of?(Hash) + "#{p[:name]}=\"#{p[:value].gsub(/"/,'"')}\"" + else + count += 1 + "#{count}=\"#{p.gsub(/"/,'"')}\"" + end + }.join(" ") + + self.data = ret.blank? ? "" : "<template __name=\"#{key}\" __hash=\"#{key_digest}\" #{tag_attr}>#{ret}</template>" + "" + end + end + end + end +end +# rubocop:enable Style/ClassAndModuleChildren +# rubocop:enable Layout/SpaceAroundEqualsInParameterDefault +# rubocop:enable Style/HashSyntax +# rubocop:enable Layout/SpaceAfterComma +# rubocop:enable Style/RescueStandardError +# rubocop:enable Rails/Output +# rubocop:enable Style/MethodCallWithoutArgsParentheses +# rubocop:enable Layout/EmptyLinesAroundClassBody +# rubocop:enable Metrics/AbcSize +# rubocop:enable Metrics/CyclomaticComplexity +# rubocop:enable Metrics/PerceivedComplexity +# rubocop:enable Cop/LineBreakAroundConditionalBlock +# rubocop:enable Layout/EmptyLineAfterGuardClause +# rubocop:enable Performance/ReverseEach +# rubocop:enable Style/BlockDelimiters +# rubocop:enable Cop/LineBreakAroundConditionalBlock +# rubocop:enable Layout/MultilineBlockLayout +# rubocop:enable Layout/BlockEndNewline diff --git a/db/migrate/20211129151155_add_migrated_to_new_structure_column_to_vulnerability_occurrences.rb b/db/migrate/20211129151155_add_migrated_to_new_structure_column_to_vulnerability_occurrences.rb new file mode 100644 index 00000000000..8c3cf82d7c7 --- /dev/null +++ b/db/migrate/20211129151155_add_migrated_to_new_structure_column_to_vulnerability_occurrences.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddMigratedToNewStructureColumnToVulnerabilityOccurrences < Gitlab::Database::Migration[1.0] + def change + add_column :vulnerability_occurrences, :migrated_to_new_structure, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20211129151832_add_index_on_vulnerability_occurrences_migrated_to_new_structure_column.rb b/db/migrate/20211129151832_add_index_on_vulnerability_occurrences_migrated_to_new_structure_column.rb new file mode 100644 index 00000000000..4cf8263f8f0 --- /dev/null +++ b/db/migrate/20211129151832_add_index_on_vulnerability_occurrences_migrated_to_new_structure_column.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddIndexOnVulnerabilityOccurrencesMigratedToNewStructureColumn < Gitlab::Database::Migration[1.0] + INDEX_NAME = 'index_vulnerability_occurrences_on_migrated_to_new_structure' + + disable_ddl_transaction! + + def up + add_concurrent_index :vulnerability_occurrences, [:migrated_to_new_structure, :id], name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :vulnerability_occurrences, INDEX_NAME + end +end diff --git a/db/post_migrate/20211201101541_drop_clusters_applications_runners_ci_runners_fk.rb b/db/post_migrate/20211201101541_drop_clusters_applications_runners_ci_runners_fk.rb new file mode 100644 index 00000000000..9a02f64e350 --- /dev/null +++ b/db/post_migrate/20211201101541_drop_clusters_applications_runners_ci_runners_fk.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class DropClustersApplicationsRunnersCiRunnersFk < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + def up + with_lock_retries do + remove_foreign_key_if_exists(:clusters_applications_runners, :ci_runners, name: 'fk_02de2ded36') + end + end + + def down + add_concurrent_foreign_key(:clusters_applications_runners, :ci_runners, name: 'fk_02de2ded36', column: :runner_id, target_column: :id, on_delete: 'set null') + end +end diff --git a/db/schema_migrations/20211129151155 b/db/schema_migrations/20211129151155 new file mode 100644 index 00000000000..4aa3e56bae6 --- /dev/null +++ b/db/schema_migrations/20211129151155 @@ -0,0 +1 @@ +c1ba97f01fca6330628090010abb54220c0d057514386c6bb867c1b6f13f252c
\ No newline at end of file diff --git a/db/schema_migrations/20211129151832 b/db/schema_migrations/20211129151832 new file mode 100644 index 00000000000..fdfc464d136 --- /dev/null +++ b/db/schema_migrations/20211129151832 @@ -0,0 +1 @@ +c6d257f635049f88cd6efba903c9384a0a1af23b3c8fe6fa7f0842dcdf9f7e39
\ No newline at end of file diff --git a/db/schema_migrations/20211201101541 b/db/schema_migrations/20211201101541 new file mode 100644 index 00000000000..52f43ddcd2f --- /dev/null +++ b/db/schema_migrations/20211201101541 @@ -0,0 +1 @@ +277cfcd1002e32c6cd664d6c0b6a7cbdf2ed7e5242e46dbddc4f99b0e8422361
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c431c35af89..d57b66ad602 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -20828,6 +20828,7 @@ CREATE TABLE vulnerability_occurrences ( cve text, location jsonb, detection_method smallint DEFAULT 0 NOT NULL, + migrated_to_new_structure boolean DEFAULT false NOT NULL, CONSTRAINT check_4a3a60f2ba CHECK ((char_length(solution) <= 7000)), CONSTRAINT check_ade261da6b CHECK ((char_length(description) <= 15000)), CONSTRAINT check_df6dd20219 CHECK ((char_length(message) <= 3000)), @@ -27780,6 +27781,8 @@ CREATE INDEX index_vulnerability_occurrences_on_location_cluster_id ON vulnerabi CREATE INDEX index_vulnerability_occurrences_on_location_image ON vulnerability_occurrences USING gin (((location -> 'image'::text))) WHERE (report_type = ANY (ARRAY[2, 7])); +CREATE INDEX index_vulnerability_occurrences_on_migrated_to_new_structure ON vulnerability_occurrences USING btree (migrated_to_new_structure, id); + CREATE INDEX index_vulnerability_occurrences_on_primary_identifier_id ON vulnerability_occurrences USING btree (primary_identifier_id); CREATE INDEX index_vulnerability_occurrences_on_project_fingerprint ON vulnerability_occurrences USING btree (project_fingerprint); @@ -28891,9 +28894,6 @@ ALTER TABLE ONLY deployments ALTER TABLE ONLY epics ADD CONSTRAINT fk_013c9f36ca FOREIGN KEY (due_date_sourcing_epic_id) REFERENCES epics(id) ON DELETE SET NULL; -ALTER TABLE ONLY clusters_applications_runners - ADD CONSTRAINT fk_02de2ded36 FOREIGN KEY (runner_id) REFERENCES ci_runners(id) ON DELETE SET NULL; - ALTER TABLE ONLY incident_management_escalation_rules ADD CONSTRAINT fk_0314ee86eb FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md index fb821824dd1..f590c6ff51c 100644 --- a/doc/api/geo_nodes.md +++ b/doc/api/geo_nodes.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Geo Nodes API **(PREMIUM SELF)** -To interact with Geo node endpoints, you need to authenticate yourself as an +To interact with Geo node endpoints, you must authenticate yourself as an administrator. ## Create a new Geo node @@ -26,7 +26,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://primary.example.com/ | Attribute | Type | Required | Description | | ----------------------------| ------- | -------- | -----------------------------------------------------------------| -| `primary` | boolean | no | Specifying whether this node will be primary. Defaults to false. | +| `primary` | boolean | no | Specifying whether this node should be primary. Defaults to false. | | `enabled` | boolean | no | Flag indicating if the Geo node is enabled. Defaults to true. | | `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in `gitlab.rb`, otherwise it must match `external_url` | | `url` | string | yes | The user-facing URL for the Geo node. | @@ -35,11 +35,11 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://primary.example.com/ | `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. Defaults to 25. | | `verification_max_capacity` | integer | no | Control the maximum concurrency of repository verification for this node. Defaults to 100. | | `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. Defaults to 10. | -| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. Defaults to false. | +| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node should replicate blobs in Object Storage. Defaults to false. | | `selective_sync_type` | string | no | Limit syncing to only specific groups or shards. Valid values: `"namespaces"`, `"shards"`, or `null`. | | `selective_sync_shards` | array | no | The repository storage for the projects synced if `selective_sync_type` == `shards`. | | `selective_sync_namespace_ids` | array | no | The IDs of groups that should be synced, if `selective_sync_type` == `namespaces`. | -| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it will be reverified. This has no effect when set on a secondary node. | +| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it is reverified. This has no effect when set on a secondary node. | Example response: @@ -199,11 +199,11 @@ PUT /geo_nodes/:id | `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. | | `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. | | `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. | -| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. | +| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node should replicate blobs in Object Storage. | | `selective_sync_type` | string | no | Limit syncing to only specific groups or shards. Valid values: `"namespaces"`, `"shards"`, or `null`. | | `selective_sync_shards` | array | no | The repository storage for the projects synced if `selective_sync_type` == `shards`. | | `selective_sync_namespace_ids` | array | no | The IDs of groups that should be synced, if `selective_sync_type` == `namespaces`. | -| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it will be reverified. This has no effect when set on a secondary node. | +| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it is reverified. This has no effect when set on a secondary node. | Example response: @@ -241,7 +241,7 @@ Example response: Removes the Geo node. NOTE: -Only a Geo primary node will accept this request. +Only a Geo primary node accepts this request. ```plaintext DELETE /geo_nodes/:id diff --git a/doc/api/invitations.md b/doc/api/invitations.md index 7ba59509f63..0bf9d106404 100644 --- a/doc/api/invitations.md +++ b/doc/api/invitations.md @@ -42,7 +42,6 @@ POST /projects/:id/invitations | `access_level` | integer | yes | A valid access level | | `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY | | `invite_source` | string | no | The source of the invitation that starts the member creation process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327120). | -| `areas_of_focus` | string | no | Areas the inviter wants the member to focus upon. | | `tasks_to_be_done` | array of strings | no | Tasks the inviter wants the member to focus on. The tasks are added as issues to a specified project. The possible values are: `ci`, `code` and `issues`. If specified, requires `tasks_project_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.6 | | `tasks_project_id` | integer | no | The project ID in which to create the task issues. If specified, requires `tasks_to_be_done`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.6 | diff --git a/doc/api/members.md b/doc/api/members.md index 48ac491dfe4..51f7b74f110 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -429,7 +429,6 @@ POST /projects/:id/members | `access_level` | integer | yes | A valid access level | | `expires_at` | string | no | A date string in the format `YEAR-MONTH-DAY` | | `invite_source` | string | no | The source of the invitation that starts the member creation process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327120). | -| `areas_of_focus` | string | no | Areas the inviter wants the member to focus upon. | | `tasks_to_be_done` | array of strings | no | Tasks the inviter wants the member to focus on. The tasks are added as issues to a specified project. The possible values are: `ci`, `code` and `issues`. If specified, requires `tasks_project_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `invite_members_for_task`. Disabled by default. | | `tasks_project_id` | integer | no | The project ID in which to create the task issues. If specified, requires `tasks_to_be_done`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `invite_members_for_task`. Disabled by default. | diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 316fd0dce16..47a2d215024 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -313,12 +313,8 @@ As in other list types, click the trash icon to remove a list. ### Iteration lists **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250479) in GitLab 13.11 [with a flag](../../administration/feature_flags.md) named `iteration_board_lists`. Enabled by default. - -FLAG: -On self-managed GitLab, by default this feature is available. To hide the feature, ask an -administrator to [disable the `iteration_board_lists` flag](../../administration/feature_flags.md). -On GitLab.com, this feature is available. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250479) in GitLab 13.11 [with a flag](../../administration/feature_flags.md) named `iteration_board_lists`. Enabled by default. +> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75404) in GitLab 14.6. Feature flag `iteration_board_lists` removed. You're also able to create lists of an iteration. These lists filter issues by the assigned iteration. diff --git a/lib/api/invitations.rb b/lib/api/invitations.rb index f7f5af07378..d78576b5d5b 100644 --- a/lib/api/invitations.rb +++ b/lib/api/invitations.rb @@ -24,7 +24,6 @@ module API requires :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)' optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'invitations-api' - optional :areas_of_focus, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Areas the inviter wants the member to focus upon' optional :tasks_to_be_done, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Tasks the inviter wants the member to do' optional :tasks_project_id, type: Integer, desc: 'The project ID in which to create the task issues' end diff --git a/lib/api/members.rb b/lib/api/members.rb index 13c8e60aa6d..4798edc4ddf 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -95,7 +95,6 @@ module API requires :user_id, types: [Integer, String], desc: 'The user ID of the new member or multiple IDs separated by commas.' optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'members-api' - optional :areas_of_focus, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Areas the inviter wants the member to focus upon' optional :tasks_to_be_done, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Tasks the inviter wants the member to do' optional :tasks_project_id, type: Integer, desc: 'The project ID in which to create the task issues' end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 218d2d41aa9..d16a6726157 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -873,6 +873,9 @@ msgstr "" msgid "%{scope} results for term '%{term}'" msgstr "" +msgid "%{search} %{description} %{scope}" +msgstr "" + msgid "%{seconds}s" msgstr "" @@ -16077,6 +16080,51 @@ msgstr "" msgid "Global notification settings" msgstr "" +msgid "GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list." +msgstr "" + +msgid "GlobalSearch|Issues I've created" +msgstr "" + +msgid "GlobalSearch|Issues assigned to me" +msgstr "" + +msgid "GlobalSearch|Merge requests I've created" +msgstr "" + +msgid "GlobalSearch|Merge requests assigned to me" +msgstr "" + +msgid "GlobalSearch|Merge requests that I'm a reviewer" +msgstr "" + +msgid "GlobalSearch|Results updated. %{count} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit." +msgstr "" + +msgid "GlobalSearch|Search GitLab" +msgstr "" + +msgid "GlobalSearch|Search or jump to..." +msgstr "" + +msgid "GlobalSearch|Search results are loading" +msgstr "" + +msgid "GlobalSearch|Type and press the enter key to submit search." +msgstr "" + +msgid "GlobalSearch|Type for new suggestions to appear below." +msgstr "" + +msgid "GlobalSearch|in all GitLab" +msgstr "" + +msgid "GlobalSearch|in group" +msgstr "" + +msgid "GlobalSearch|in project" +msgstr "" + msgid "Go Back" msgstr "" @@ -18843,6 +18891,9 @@ msgstr "" msgid "Integrations|can't exceed %{recipients_limit}" msgstr "" +msgid "Interactive developer security education." +msgstr "" + msgid "Interactive mode" msgstr "" @@ -19071,21 +19122,9 @@ msgstr "" msgid "InviteMembersModal|Close invite team members" msgstr "" -msgid "InviteMembersModal|Collaborate on open issues and merge requests" -msgstr "" - -msgid "InviteMembersModal|Configure CI/CD" -msgstr "" - -msgid "InviteMembersModal|Configure security features" -msgstr "" - msgid "InviteMembersModal|Congratulations on creating your project, you're almost there!" msgstr "" -msgid "InviteMembersModal|Contribute to the codebase" -msgstr "" - msgid "InviteMembersModal|Create issues for your new team member to work on (optional)" msgstr "" @@ -19110,9 +19149,6 @@ msgstr "" msgid "InviteMembersModal|Members were successfully added" msgstr "" -msgid "InviteMembersModal|Other" -msgstr "" - msgid "InviteMembersModal|Search for a group to invite" msgstr "" @@ -19131,9 +19167,6 @@ msgstr "" msgid "InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}" msgstr "" -msgid "InviteMembersModal|What would you like new member(s) to focus on? (optional)" -msgstr "" - msgid "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group." msgstr "" @@ -19455,9 +19488,6 @@ msgstr "" msgid "Issues" msgstr "" -msgid "Issues I've created" -msgstr "" - msgid "Issues Rate Limits" msgstr "" @@ -19467,9 +19497,6 @@ msgstr "" msgid "Issues are being rebalanced at the moment, so manual reordering is disabled." msgstr "" -msgid "Issues assigned to me" -msgstr "" - msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable." msgstr "" @@ -20196,6 +20223,9 @@ msgstr "" msgid "Ki" msgstr "" +msgid "Kontra" +msgstr "" + msgid "Kroki" msgstr "" @@ -21859,18 +21889,9 @@ msgstr "" msgid "Merge requests" msgstr "" -msgid "Merge requests I've created" -msgstr "" - msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others" msgstr "" -msgid "Merge requests assigned to me" -msgstr "" - -msgid "Merge requests that I'm a reviewer" -msgstr "" - msgid "Merge the branch and fix any conflicts that come up" msgstr "" @@ -30671,6 +30692,9 @@ msgstr "" msgid "Secure token that identifies an external storage request." msgstr "" +msgid "SecureCodeWarrior" +msgstr "" + msgid "Security" msgstr "" @@ -30695,6 +30719,9 @@ msgstr "" msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})" msgstr "" +msgid "Security training with guide and learning pathways." +msgstr "" + msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability." msgstr "" @@ -36606,6 +36633,9 @@ msgstr "" msgid "Track your GitLab projects with GitLab for Slack." msgstr "" +msgid "Training mode" +msgstr "" + msgid "Transfer" msgstr "" @@ -41231,18 +41261,9 @@ msgstr "" msgid "in" msgstr "" -msgid "in all GitLab" -msgstr "" - -msgid "in group" -msgstr "" - msgid "in group %{link_to_group}" msgstr "" -msgid "in project" -msgstr "" - msgid "in project %{link_to_project}" msgstr "" diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb index b5cd14154a3..4a6e745cd63 100644 --- a/spec/controllers/repositories/git_http_controller_spec.rb +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -90,6 +90,14 @@ RSpec.describe Repositories::GitHttpController do end end end + + context 'when the user is a deploy token' do + it_behaves_like Repositories::GitHttpController do + let(:container) { project } + let(:user) { create(:deploy_token, :project, projects: [project]) } + let(:access_checker_class) { Gitlab::GitAccess } + end + end end context 'when repository container is a project wiki' do diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb index 1cae7cdeb16..0ce50107e54 100644 --- a/spec/features/groups/members/manage_members_spec.rb +++ b/spec/features/groups/members/manage_members_spec.rb @@ -85,33 +85,6 @@ RSpec.describe 'Groups > Members > Manage members' do property: 'existing_user', user: user1 ) - expect_no_snowplow_event( - category: 'Members::CreateService', - action: 'area_of_focus' - ) - end - - it 'adds a user to group with area_of_focus', :js, :snowplow, :aggregate_failures do - stub_experiments(member_areas_of_focus: :candidate) - group.add_owner(user1) - - visit group_group_members_path(group) - - invite_member(user2.name, role: 'Reporter', area_of_focus: true) - wait_for_requests - - expect_snowplow_event( - category: 'Members::CreateService', - action: 'area_of_focus', - label: 'Contribute to the codebase', - property: group.members.last.id.to_s - ) - expect_snowplow_event( - category: 'Members::CreateService', - action: 'area_of_focus', - label: 'Collaborate on open issues and merge requests', - property: group.members.last.id.to_s - ) end it 'do not disclose email addresses', :js do @@ -221,36 +194,9 @@ RSpec.describe 'Groups > Members > Manage members' do property: 'net_new_user', user: user1 ) - expect_no_snowplow_event( - category: 'Members::CreateService', - action: 'area_of_focus' - ) end end - it 'invite user to group with area_of_focus', :js, :snowplow, :aggregate_failures do - stub_experiments(member_areas_of_focus: :candidate) - group.add_owner(user1) - - visit group_group_members_path(group) - - invite_member('test@example.com', role: 'Reporter', area_of_focus: true) - wait_for_requests - - expect_snowplow_event( - category: 'Members::InviteService', - action: 'area_of_focus', - label: 'Contribute to the codebase', - property: group.members.last.id.to_s - ) - expect_snowplow_event( - category: 'Members::InviteService', - action: 'area_of_focus', - label: 'Collaborate on open issues and merge requests', - property: group.members.last.id.to_s - ) - end - context 'when user is a guest' do before do group.add_guest(user1) diff --git a/spec/features/one_trust_spec.rb b/spec/features/one_trust_spec.rb new file mode 100644 index 00000000000..0ed08e8b99b --- /dev/null +++ b/spec/features/one_trust_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'OneTrust' do + context 'almost there page' do + context 'when OneTrust is enabled' do + let_it_be(:onetrust_url) { 'https://*.onetrust.com' } + let_it_be(:one_trust_id) { SecureRandom.uuid } + + before do + stub_config(extra: { one_trust_id: one_trust_id }) + stub_feature_flags(ecomm_instrumentation: true) + visit users_almost_there_path + end + + it 'has the OneTrust CSP settings', :aggregate_failures do + expect(response_headers['Content-Security-Policy']).to include("#{onetrust_url}") + expect(page.html).to include("https://cdn.cookielaw.org/consent/#{one_trust_id}/OtAutoBlock.js") + end + end + end +end diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js index 7ecbef8af4b..194846c410a 100644 --- a/spec/frontend/header_search/components/app_spec.js +++ b/spec/frontend/header_search/components/app_spec.js @@ -6,6 +6,7 @@ import HeaderSearchApp from '~/header_search/components/app.vue'; import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue'; import HeaderSearchDefaultItems from '~/header_search/components/header_search_default_items.vue'; import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue'; +import { SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION } from '~/header_search/constants'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; import { ENTER_KEY } from '~/lib/utils/keys'; import { visitUrl } from '~/lib/utils/url_utility'; @@ -14,6 +15,7 @@ import { MOCK_SEARCH_QUERY, MOCK_USERNAME, MOCK_DEFAULT_SEARCH_OPTIONS, + MOCK_SCOPED_SEARCH_OPTIONS, } from '../mock_data'; Vue.use(Vuex); @@ -59,11 +61,26 @@ describe('HeaderSearchApp', () => { const findHeaderSearchAutocompleteItems = () => wrapper.findComponent(HeaderSearchAutocompleteItems); const findDropdownKeyboardNavigation = () => wrapper.findComponent(DropdownKeyboardNavigation); + const findSearchInputDescription = () => wrapper.find(`#${SEARCH_INPUT_DESCRIPTION}`); + const findSearchResultsDescription = () => wrapper.findByTestId(SEARCH_RESULTS_DESCRIPTION); describe('template', () => { - it('always renders Header Search Input', () => { - createComponent(); - expect(findHeaderSearchInput().exists()).toBe(true); + describe('always renders', () => { + beforeEach(() => { + createComponent(); + }); + + it('Header Search Input', () => { + expect(findHeaderSearchInput().exists()).toBe(true); + }); + + it('Search Input Description', () => { + expect(findSearchInputDescription().exists()).toBe(true); + }); + + it('Search Results Description', () => { + expect(findSearchResultsDescription().exists()).toBe(true); + }); }); describe.each` @@ -77,7 +94,7 @@ describe('HeaderSearchApp', () => { beforeEach(() => { window.gon.current_username = username; createComponent(); - wrapper.setData({ showDropdown }); + findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : ''); }); it(`should${showSearchDropdown ? '' : ' not'} render`, () => { @@ -123,6 +140,53 @@ describe('HeaderSearchApp', () => { }); }, ); + + describe.each` + username | showDropdown | expectedDesc + ${null} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown} + ${null} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown} + ${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown} + ${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown} + `('Search Input Description', ({ username, showDropdown, expectedDesc }) => { + describe(`current_username is ${username} and showDropdown is ${showDropdown}`, () => { + beforeEach(() => { + window.gon.current_username = username; + createComponent(); + findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : ''); + }); + + it(`sets description to ${expectedDesc}`, () => { + expect(findSearchInputDescription().text()).toBe(expectedDesc); + }); + }); + }); + + describe.each` + username | showDropdown | search | loading | searchOptions | expectedDesc + ${null} | ${true} | ${''} | ${false} | ${[]} | ${''} + ${MOCK_USERNAME} | ${false} | ${''} | ${false} | ${[]} | ${''} + ${MOCK_USERNAME} | ${true} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`} + ${MOCK_USERNAME} | ${true} | ${''} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`} + ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`} + ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.searchResultsLoading} + `( + 'Search Results Description', + ({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => { + describe(`search is ${search}, loading is ${loading}, and showSearchDropdown is ${ + Boolean(username) && showDropdown + }`, () => { + beforeEach(() => { + window.gon.current_username = username; + createComponent({ search, loading }, { searchOptions: () => searchOptions }); + findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : ''); + }); + + it(`sets description to ${expectedDesc}`, () => { + expect(findSearchResultsDescription().text()).toBe(expectedDesc); + }); + }); + }, + ); }); describe('events', () => { diff --git a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js index ecfaa8e9443..bec0cbc8a5c 100644 --- a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js +++ b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js @@ -110,11 +110,11 @@ describe('HeaderSearchAutocompleteItems', () => { }); describe.each` - currentFocusedOption | isFocused - ${null} | ${false} - ${{ html_id: 'not-a-match' }} | ${false} - ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0]} | ${true} - `('isOptionFocused', ({ currentFocusedOption, isFocused }) => { + currentFocusedOption | isFocused | ariaSelected + ${null} | ${false} | ${undefined} + ${{ html_id: 'not-a-match' }} | ${false} | ${undefined} + ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0]} | ${true} | ${'true'} + `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => { describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => { beforeEach(() => { createComponent({}, {}, { currentFocusedOption }); @@ -123,6 +123,10 @@ describe('HeaderSearchAutocompleteItems', () => { it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => { expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused); }); + + it(`sets "aria-selected to ${ariaSelected}`, () => { + expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected); + }); }); }); }); diff --git a/spec/frontend/header_search/components/header_search_default_items_spec.js b/spec/frontend/header_search/components/header_search_default_items_spec.js index afad9b5b138..abcacc487df 100644 --- a/spec/frontend/header_search/components/header_search_default_items_spec.js +++ b/spec/frontend/header_search/components/header_search_default_items_spec.js @@ -83,11 +83,11 @@ describe('HeaderSearchDefaultItems', () => { }); describe.each` - currentFocusedOption | isFocused - ${null} | ${false} - ${{ html_id: 'not-a-match' }} | ${false} - ${MOCK_DEFAULT_SEARCH_OPTIONS[0]} | ${true} - `('isOptionFocused', ({ currentFocusedOption, isFocused }) => { + currentFocusedOption | isFocused | ariaSelected + ${null} | ${false} | ${undefined} + ${{ html_id: 'not-a-match' }} | ${false} | ${undefined} + ${MOCK_DEFAULT_SEARCH_OPTIONS[0]} | ${true} | ${'true'} + `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => { describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => { beforeEach(() => { createComponent({}, { currentFocusedOption }); @@ -96,6 +96,10 @@ describe('HeaderSearchDefaultItems', () => { it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => { expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused); }); + + it(`sets "aria-selected to ${ariaSelected}`, () => { + expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected); + }); }); }); }); diff --git a/spec/frontend/header_search/components/header_search_scoped_items_spec.js b/spec/frontend/header_search/components/header_search_scoped_items_spec.js index 2fdd03fdae3..a65b4d8b813 100644 --- a/spec/frontend/header_search/components/header_search_scoped_items_spec.js +++ b/spec/frontend/header_search/components/header_search_scoped_items_spec.js @@ -37,6 +37,8 @@ describe('HeaderSearchScopedItems', () => { const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findFirstDropdownItem = () => findDropdownItems().at(0); const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text())); + const findDropdownItemAriaLabels = () => + findDropdownItems().wrappers.map((w) => trimText(w.attributes('aria-label'))); const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); describe('template', () => { @@ -56,6 +58,13 @@ describe('HeaderSearchScopedItems', () => { expect(findDropdownItemTitles()).toStrictEqual(expectedTitles); }); + it('renders aria-labels correctly', () => { + const expectedLabels = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => + trimText(`${MOCK_SEARCH} ${o.description} ${o.scope || ''}`), + ); + expect(findDropdownItemAriaLabels()).toStrictEqual(expectedLabels); + }); + it('renders links correctly', () => { const expectedLinks = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => o.url); expect(findDropdownItemLinks()).toStrictEqual(expectedLinks); @@ -63,11 +72,11 @@ describe('HeaderSearchScopedItems', () => { }); describe.each` - currentFocusedOption | isFocused - ${null} | ${false} - ${{ html_id: 'not-a-match' }} | ${false} - ${MOCK_SCOPED_SEARCH_OPTIONS[0]} | ${true} - `('isOptionFocused', ({ currentFocusedOption, isFocused }) => { + currentFocusedOption | isFocused | ariaSelected + ${null} | ${false} | ${undefined} + ${{ html_id: 'not-a-match' }} | ${false} | ${undefined} + ${MOCK_SCOPED_SEARCH_OPTIONS[0]} | ${true} | ${'true'} + `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => { describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => { beforeEach(() => { createComponent({}, { currentFocusedOption }); @@ -76,6 +85,10 @@ describe('HeaderSearchScopedItems', () => { it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => { expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused); }); + + it(`sets "aria-selected to ${ariaSelected}`, () => { + expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected); + }); }); }); }); diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js index 24df424b40b..f3cad3fc066 100644 --- a/spec/frontend/invite_members/components/invite_members_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js @@ -6,7 +6,6 @@ import { GlSprintf, GlLink, GlModal, - GlFormCheckboxGroup, } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import { stubComponent } from 'helpers/stub_component'; @@ -19,7 +18,6 @@ import ModalConfetti from '~/invite_members/components/confetti.vue'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; import { INVITE_MEMBERS_IN_COMMENT, - MEMBER_AREAS_OF_FOCUS, INVITE_MEMBERS_FOR_TASK, CANCEL_BUTTON_TEXT, INVITE_BUTTON_TEXT, @@ -52,12 +50,7 @@ const inviteeType = 'members'; const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 }; const defaultAccessLevel = 10; const inviteSource = 'unknown'; -const noSelectionAreasOfFocus = ['no_selection']; const helpLink = 'https://example.com'; -const areasOfFocusOptions = [ - { text: 'area1', value: 'area1' }, - { text: 'area2', value: 'area2' }, -]; const tasksToBeDoneOptions = [ { text: 'First task', value: 'first' }, { text: 'Second task', value: 'second' }, @@ -96,9 +89,7 @@ const createComponent = (data = {}, props = {}) => { isProject, inviteeType, accessLevels, - areasOfFocusOptions, defaultAccessLevel, - noSelectionAreasOfFocus, tasksToBeDoneOptions, projects, helpLink, @@ -164,7 +155,6 @@ describe('InviteMembersModal', () => { const membersFormGroupInvalidFeedback = () => findMembersFormGroup().props('invalidFeedback'); const membersFormGroupDescription = () => findMembersFormGroup().props('description'); const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect); - const findAreaofFocusCheckBoxGroup = () => wrapper.findComponent(GlFormCheckboxGroup); const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done'); const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks'); const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select'); @@ -215,21 +205,6 @@ describe('InviteMembersModal', () => { }); }); - describe('rendering the areas_of_focus', () => { - it('renders the areas_of_focus checkboxes', () => { - createComponent(); - - expect(findAreaofFocusCheckBoxGroup().props('options')).toBe(areasOfFocusOptions); - expect(findAreaofFocusCheckBoxGroup().exists()).toBe(true); - }); - - it('does not render the areas_of_focus checkboxes', () => { - createComponent({}, { areasOfFocusOptions: [] }); - - expect(findAreaofFocusCheckBoxGroup().exists()).toBe(false); - }); - }); - describe('rendering the tasks to be done', () => { const setupComponent = ( extraData = {}, @@ -442,20 +417,6 @@ describe('InviteMembersModal', () => { "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups."; const expectedSyntaxError = 'email contains an invalid email address'; - it('calls the API with the expected focus data when an areas_of_focus checkbox is clicked', () => { - const spy = jest.spyOn(Api, 'addGroupMembersByUserId'); - const expectedFocus = [areasOfFocusOptions[0].value]; - createComponent({ newUsersToInvite: [user1] }); - - findAreaofFocusCheckBoxGroup().vm.$emit('input', expectedFocus); - clickInviteButton(); - - expect(spy).toHaveBeenCalledWith( - user1.id.toString(), - expect.objectContaining({ areas_of_focus: expectedFocus }), - ); - }); - describe('when inviting an existing user to group by user ID', () => { const postData = { user_id: '1,2', @@ -463,7 +424,6 @@ describe('InviteMembersModal', () => { expires_at: undefined, invite_source: inviteSource, format: 'json', - areas_of_focus: noSelectionAreasOfFocus, tasks_to_be_done: [], tasks_project_id: '', }; @@ -476,16 +436,6 @@ describe('InviteMembersModal', () => { jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData }); }); - it('includes the non-default selected areas of focus', () => { - const focus = ['abc']; - const updatedPostData = { ...postData, areas_of_focus: focus }; - wrapper.setData({ selectedAreasOfFocus: focus }); - - clickInviteButton(); - - expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, updatedPostData); - }); - describe('when triggered from regular mounting', () => { beforeEach(() => { clickInviteButton(); @@ -661,7 +611,6 @@ describe('InviteMembersModal', () => { expires_at: undefined, email: 'email@example.com', invite_source: inviteSource, - areas_of_focus: noSelectionAreasOfFocus, tasks_to_be_done: [], tasks_project_id: '', format: 'json', @@ -675,16 +624,6 @@ describe('InviteMembersModal', () => { jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData }); }); - it('includes the non-default selected areas of focus', () => { - const focus = ['abc']; - const updatedPostData = { ...postData, areas_of_focus: focus }; - wrapper.setData({ selectedAreasOfFocus: focus }); - - clickInviteButton(); - - expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, updatedPostData); - }); - describe('when triggered from regular mounting', () => { beforeEach(() => { clickInviteButton(); @@ -792,7 +731,6 @@ describe('InviteMembersModal', () => { access_level: defaultAccessLevel, expires_at: undefined, invite_source: inviteSource, - areas_of_focus: noSelectionAreasOfFocus, format: 'json', tasks_to_be_done: [], tasks_project_id: '', @@ -951,30 +889,12 @@ describe('InviteMembersModal', () => { expect(ExperimentTracking).not.toHaveBeenCalledWith(INVITE_MEMBERS_IN_COMMENT); }); - it('tracks the view for areas_of_focus', () => { - eventHub.$emit('openModal', { inviteeType: 'members' }); - - expect(ExperimentTracking).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.name); - expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.view); - }); - it('tracks the view for learn_gitlab source', () => { eventHub.$emit('openModal', { inviteeType: 'members', source: LEARN_GITLAB }); expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name); expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(LEARN_GITLAB); }); - - it('tracks the invite for areas_of_focus', () => { - eventHub.$emit('openModal', { inviteeType: 'members' }); - - clickInviteButton(); - - expect(ExperimentTracking).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.name); - expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith( - MEMBER_AREAS_OF_FOCUS.submit, - ); - }); }); }); }); diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js index 4e0ea6a9717..759844b941a 100644 --- a/spec/frontend/security_configuration/components/app_spec.js +++ b/spec/frontend/security_configuration/components/app_spec.js @@ -5,7 +5,10 @@ import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser'; import stubChildren from 'helpers/stub_children'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import SecurityConfigurationApp, { i18n } from '~/security_configuration/components/app.vue'; +import SecurityConfigurationApp, { + i18n, + TRAINING_PROVIDERS, +} from '~/security_configuration/components/app.vue'; import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_alert.vue'; import AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev_ops_enabled_alert.vue'; import { @@ -20,6 +23,7 @@ import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY, } from '~/security_configuration/components/constants'; import FeatureCard from '~/security_configuration/components/feature_card.vue'; +import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue'; import { @@ -78,6 +82,7 @@ describe('App component', () => { const findTabs = () => wrapper.findAllComponents(GlTab); const findByTestId = (id) => wrapper.findByTestId(id); const findFeatureCards = () => wrapper.findAllComponents(FeatureCard); + const findTrainingProviderList = () => wrapper.findComponent(TrainingProviderList); const findManageViaMRErrorAlert = () => wrapper.findByTestId('manage-via-mr-error-alert'); const findLink = ({ href, text, container = wrapper }) => { const selector = `a[href="${href}"]`; @@ -180,6 +185,10 @@ describe('App component', () => { expect(findComplianceViewHistoryLink().exists()).toBe(false); expect(findSecurityViewHistoryLink().exists()).toBe(false); }); + + it('renders training provider list with correct props', () => { + expect(findTrainingProviderList().props('providers')).toEqual(TRAINING_PROVIDERS); + }); }); describe('Manage via MR Error Alert', () => { diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js new file mode 100644 index 00000000000..1169a977d44 --- /dev/null +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -0,0 +1,60 @@ +import { GlLink, GlToggle, GlCard } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; +import { TRAINING_PROVIDERS } from '~/security_configuration/components/app.vue'; + +const DEFAULT_PROPS = { + providers: TRAINING_PROVIDERS, +}; + +describe('TrainingProviderList component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(TrainingProviderList, { + propsData: { + ...DEFAULT_PROPS, + ...props, + }, + }); + }; + + const findCards = () => wrapper.findAllComponents(GlCard); + const findLinks = () => wrapper.findAllComponents(GlLink); + const findToggles = () => wrapper.findAllComponents(GlToggle); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('basic structure', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders correct amount of cards', () => { + expect(findCards()).toHaveLength(DEFAULT_PROPS.providers.length); + }); + + DEFAULT_PROPS.providers.forEach(({ name, description, url, isEnabled }, index) => { + it(`shows the name for card ${index}`, () => { + expect(findCards().at(index).text()).toContain(name); + }); + + it(`shows the description for card ${index}`, () => { + expect(findCards().at(index).text()).toContain(description); + }); + + it(`shows the learn more link for card ${index}`, () => { + expect(findLinks().at(index).attributes()).toEqual({ + target: '_blank', + href: url, + }); + }); + + it(`shows the toggle with the correct value for card ${index}`, () => { + expect(findToggles().at(index).props('value')).toEqual(isEnabled); + }); + }); + }); +}); diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb index e3db2240318..d8a97b93bc9 100644 --- a/spec/helpers/invite_members_helper_spec.rb +++ b/spec/helpers/invite_members_helper_spec.rb @@ -16,52 +16,14 @@ RSpec.describe InviteMembersHelper do end describe '#common_invite_modal_dataset' do - context 'when member_areas_of_focus is enabled', :experiment do - context 'with control experience' do - before do - stub_experiments(member_areas_of_focus: :control) - end - - it 'has expected attributes' do - attributes = { - areas_of_focus_options: [], - no_selection_areas_of_focus: [] - } - - expect(helper.common_invite_modal_dataset(project)).to include(attributes) - end - end - - context 'with candidate experience' do - before do - stub_experiments(member_areas_of_focus: :candidate) - end - - it 'has expected attributes', :aggregate_failures do - output = helper.common_invite_modal_dataset(project) - - expect(output[:no_selection_areas_of_focus]).to eq ['no_selection'] - expect(Gitlab::Json.parse(output[:areas_of_focus_options]).first['value']).to eq 'Contribute to the codebase' - end - end - end - - context 'when member_areas_of_focus is disabled' do - before do - stub_feature_flags(member_areas_of_focus: false) - end - - it 'has expected attributes' do - attributes = { - id: project.id, - name: project.name, - default_access_level: Gitlab::Access::GUEST, - areas_of_focus_options: [], - no_selection_areas_of_focus: [] - } - - expect(helper.common_invite_modal_dataset(project)).to include(attributes) - end + it 'has expected common attributes' do + attributes = { + id: project.id, + name: project.name, + default_access_level: Gitlab::Access::GUEST + } + + expect(helper.common_invite_modal_dataset(project)).to include(attributes) end context 'tasks_to_be_done' do diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb index 7b17a5ed1cb..702e6ef0a2a 100644 --- a/spec/requests/api/invitations_spec.rb +++ b/spec/requests/api/invitations_spec.rb @@ -152,20 +152,6 @@ RSpec.describe API::Invitations do end end - context 'with areas_of_focus', :snowplow do - it 'tracks the areas_of_focus from params' do - post invitations_url(source, maintainer), - params: { email: email, access_level: Member::DEVELOPER, areas_of_focus: 'Other' } - - expect_snowplow_event( - category: 'Members::InviteService', - action: 'area_of_focus', - label: 'Other', - property: source.members.last.id.to_s - ) - end - end - context 'with tasks_to_be_done and tasks_project_id in the params' do let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id } diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index bf5b89e72c6..02061bb8ab6 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -387,33 +387,6 @@ RSpec.describe API::Members do end end - context 'with areas_of_focus considerations', :snowplow do - let(:user_id) { stranger.id } - - context 'when areas_of_focus is present in params' do - it 'tracks the areas_of_focus' do - post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), - params: { user_id: user_id, access_level: Member::DEVELOPER, areas_of_focus: 'Other' } - - expect_snowplow_event( - category: 'Members::CreateService', - action: 'area_of_focus', - label: 'Other', - property: source.members.last.id.to_s - ) - end - end - - context 'when areas_of_focus is not present in params' do - it 'does not track the areas_of_focus' do - post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), - params: { user_id: user_id, access_level: Member::DEVELOPER } - - expect_no_snowplow_event(category: 'Members::CreateService', action: 'area_of_focus') - end - end - end - context 'with tasks_to_be_done and tasks_project_id in the params' do let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id } diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb index babd601e0cf..34f77260334 100644 --- a/spec/services/ci/play_build_service_spec.rb +++ b/spec/services/ci/play_build_service_spec.rb @@ -79,12 +79,22 @@ RSpec.describe Ci::PlayBuildService, '#execute' do { key: 'second', secret_value: 'second' }] end + subject { service.execute(build, job_variables) } + it 'assigns the variables to the build' do - service.execute(build, job_variables) + subject expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second') end + context 'when variables are invalid' do + let(:job_variables) { [{}] } + + it 'raises an error' do + expect { subject }.to raise_error(ActiveRecord::RecordInvalid) + end + end + context 'when user defined variables are restricted' do before do project.update!(restrict_user_defined_variables: true) @@ -96,7 +106,7 @@ RSpec.describe Ci::PlayBuildService, '#execute' do end it 'assigns the variables to the build' do - service.execute(build, job_variables) + subject expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second') end @@ -104,8 +114,7 @@ RSpec.describe Ci::PlayBuildService, '#execute' do context 'when user is developer' do it 'raises an error' do - expect { service.execute(build, job_variables) } - .to raise_error Gitlab::Access::AccessDeniedError + expect { subject }.to raise_error Gitlab::Access::AccessDeniedError end end end diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index 43493deb621..13f56fe7458 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -127,76 +127,6 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ end end - context 'when tracking the areas of focus', :snowplow do - context 'when areas_of_focus is not passed' do - it 'does not track' do - execute_service - - expect_no_snowplow_event(category: described_class.name, action: 'area_of_focus') - end - end - - context 'when 1 areas_of_focus is passed' do - let(:additional_params) { { invite_source: '_invite_source_', areas_of_focus: ['no_selection'] } } - - it 'tracks the areas_of_focus from params' do - execute_service - - expect_snowplow_event( - category: described_class.name, - action: 'area_of_focus', - label: 'no_selection', - property: source.members.last.id.to_s - ) - end - - context 'when passing many user ids' do - let(:another_user) { create(:user) } - let(:user_ids) { [member.id, another_user.id].join(',') } - - it 'tracks the areas_of_focus from params' do - execute_service - - members = source.members.last(2) - - expect_snowplow_event( - category: described_class.name, - action: 'area_of_focus', - label: 'no_selection', - property: members.first.id.to_s - ) - expect_snowplow_event( - category: described_class.name, - action: 'area_of_focus', - label: 'no_selection', - property: members.last.id.to_s - ) - end - end - end - - context 'when multiple areas_of_focus are passed' do - let(:additional_params) { { invite_source: '_invite_source_', areas_of_focus: %w[no_selection Other] } } - - it 'tracks the areas_of_focus from params' do - execute_service - - expect_snowplow_event( - category: described_class.name, - action: 'area_of_focus', - label: 'no_selection', - property: source.members.last.id.to_s - ) - expect_snowplow_event( - category: described_class.name, - action: 'area_of_focus', - label: 'Other', - property: source.members.last.id.to_s - ) - end - end - end - context 'when assigning tasks to be done' do let(:additional_params) do { invite_source: '_invite_source_', tasks_to_be_done: %w(ci code), tasks_project_id: source.id } diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb index 3502558b2c2..11040562b49 100644 --- a/spec/support/helpers/features/invite_members_modal_helper.rb +++ b/spec/support/helpers/features/invite_members_modal_helper.rb @@ -5,7 +5,7 @@ module Spec module Helpers module Features module InviteMembersModalHelper - def invite_member(name, role: 'Guest', expires_at: nil, area_of_focus: false) + def invite_member(name, role: 'Guest', expires_at: nil) click_on 'Invite members' page.within '[data-testid="invite-members-modal"]' do @@ -14,7 +14,6 @@ module Spec wait_for_requests click_button name choose_options(role, expires_at) - choose_area_of_focus if area_of_focus click_button 'Invite' @@ -44,13 +43,6 @@ module Spec fill_in 'YYYY-MM-DD', with: expires_at.strftime('%Y-%m-%d') if expires_at end - - def choose_area_of_focus - page.within '[data-testid="area-of-focus-checks"]' do - check 'Contribute to the codebase' - check 'Collaborate on open issues and merge requests' - end - end end end end diff --git a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb index 00a0fb7e4c5..3a7588a5cc9 100644 --- a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb @@ -50,7 +50,8 @@ RSpec.shared_examples Repositories::GitHttpController do context 'with authorized user' do before do - request.headers.merge! auth_env(user.username, user.password, nil) + password = user.try(:password) || user.try(:token) + request.headers.merge! auth_env(user.username, password, nil) end it 'returns 200' do @@ -71,9 +72,10 @@ RSpec.shared_examples Repositories::GitHttpController do it 'adds user info to the logs' do get :info_refs, params: params - expect(log_data).to include('username' => user.username, - 'user_id' => user.id, - 'meta.user' => user.username) + user_log_data = { 'username' => user.username, 'user_id' => user.id } + user_log_data['meta.user'] = user.username if user.is_a?(User) + + expect(log_data).to include(user_log_data) end end end |