diff options
143 files changed, 1156 insertions, 644 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index d34b133edee..19f0b0b294f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -254,7 +254,6 @@ Gitlab/HTTParty: Gitlab/Json: Enabled: true Exclude: - - 'db/**/*' - 'qa/**/*' - 'scripts/**/*' - 'tooling/rspec_flaky/**/*' diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index d52cf65a704..7fceb2eecaa 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -90,45 +90,6 @@ Graphql/OldTypes: - 'app/graphql/mutations/snippets/create.rb' - 'app/graphql/mutations/snippets/update.rb' - 'app/graphql/mutations/user_callouts/create.rb' - - 'app/graphql/resolvers/alert_management/alert_resolver.rb' - - 'app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb' - - 'app/graphql/resolvers/blobs_resolver.rb' - - 'app/graphql/resolvers/ci/config_resolver.rb' - - 'app/graphql/resolvers/ci/runners_resolver.rb' - - 'app/graphql/resolvers/ci/template_resolver.rb' - - 'app/graphql/resolvers/concerns/group_issuable_resolver.rb' - - 'app/graphql/resolvers/concerns/issue_resolver_arguments.rb' - - 'app/graphql/resolvers/concerns/resolves_pipelines.rb' - - 'app/graphql/resolvers/container_repositories_resolver.rb' - - 'app/graphql/resolvers/design_management/design_resolver.rb' - - 'app/graphql/resolvers/design_management/version/design_at_version_resolver.rb' - - 'app/graphql/resolvers/design_management/version_in_collection_resolver.rb' - - 'app/graphql/resolvers/design_management/versions_resolver.rb' - - 'app/graphql/resolvers/environments_resolver.rb' - - 'app/graphql/resolvers/full_path_resolver.rb' - - 'app/graphql/resolvers/group_labels_resolver.rb' - - 'app/graphql/resolvers/group_milestones_resolver.rb' - - 'app/graphql/resolvers/labels_resolver.rb' - - 'app/graphql/resolvers/members_resolver.rb' - - 'app/graphql/resolvers/merge_request_resolver.rb' - - 'app/graphql/resolvers/merge_requests_resolver.rb' - - 'app/graphql/resolvers/metrics/dashboard_resolver.rb' - - 'app/graphql/resolvers/milestones_resolver.rb' - - 'app/graphql/resolvers/namespace_projects_resolver.rb' - - 'app/graphql/resolvers/packages_base_resolver.rb' - - 'app/graphql/resolvers/project_milestones_resolver.rb' - - 'app/graphql/resolvers/project_pipeline_resolver.rb' - - 'app/graphql/resolvers/projects/jira_projects_resolver.rb' - - 'app/graphql/resolvers/projects/services_resolver.rb' - - 'app/graphql/resolvers/projects_resolver.rb' - - 'app/graphql/resolvers/release_resolver.rb' - - 'app/graphql/resolvers/repository_branch_names_resolver.rb' - - 'app/graphql/resolvers/snippets_resolver.rb' - - 'app/graphql/resolvers/terraform/states_resolver.rb' - - 'app/graphql/resolvers/tree_resolver.rb' - - 'app/graphql/resolvers/user_resolver.rb' - - 'app/graphql/resolvers/user_starred_projects_resolver.rb' - - 'app/graphql/resolvers/users_resolver.rb' - 'app/graphql/types/access_level_type.rb' - 'app/graphql/types/admin/analytics/usage_trends/measurement_type.rb' - 'app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index eb5cb835516..fa3999e2a82 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -996a4adda765e8ced18c72eca0ebd27848afa3c9 +818f3d85a2c8e6596376f1d2276aa22660203a6c diff --git a/app/assets/javascripts/diffs/components/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue index 178f93b651e..2d9ac76b3e4 100644 --- a/app/assets/javascripts/diffs/components/settings_dropdown.vue +++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue @@ -60,14 +60,14 @@ export default { <gl-button :class="{ selected: !renderTreeList }" class="gl-w-half js-list-view" - @click="setRenderTreeList(false)" + @click="setRenderTreeList({ renderTreeList: false })" > {{ __('List view') }} </gl-button> <gl-button :class="{ selected: renderTreeList }" class="gl-w-half js-tree-view" - @click="setRenderTreeList(true)" + @click="setRenderTreeList({ renderTreeList: true })" > {{ __('Tree view') }} </gl-button> diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index ea83523008c..9d974c132b5 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -93,7 +93,7 @@ export default function initDiffsApp(store) { const treeListStored = localStorage.getItem(TREE_LIST_STORAGE_KEY); const renderTreeList = treeListStored !== null ? parseBoolean(treeListStored) : true; - this.setRenderTreeList(renderTreeList); + this.setRenderTreeList({ renderTreeList, trackClick: false }); // NOTE: A "true" or "checked" value for `showWhitespace` is '0' not '1'. // Check for cookie and save that setting for future use. @@ -104,6 +104,7 @@ export default function initDiffsApp(store) { this.setShowWhitespace({ url: this.endpointUpdateUser, showWhitespace: hideWhitespace !== '1', + trackClick: false, }); Cookies.remove(DIFF_WHITESPACE_COOKIE_NAME); } else { @@ -111,6 +112,7 @@ export default function initDiffsApp(store) { this.setShowWhitespace({ showWhitespace: this.showWhitespaceDefault, updateDatabase: false, + trackClick: false, }); } }, diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 66510edf3db..f8cc39fc238 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -560,12 +560,12 @@ export const closeDiffFileCommentForm = ({ commit }, fileHash) => { commit(types.CLOSE_DIFF_FILE_COMMENT_FORM, fileHash); }; -export const setRenderTreeList = ({ commit }, renderTreeList) => { +export const setRenderTreeList = ({ commit }, { renderTreeList, trackClick = true }) => { commit(types.SET_RENDER_TREE_LIST, renderTreeList); localStorage.setItem(TREE_LIST_STORAGE_KEY, renderTreeList); - if (window.gon?.features?.diffSettingsUsageData) { + if (window.gon?.features?.diffSettingsUsageData && trackClick) { api.trackRedisHllUserEvent(TRACKING_CLICK_FILE_BROWSER_SETTING); if (renderTreeList) { @@ -578,7 +578,7 @@ export const setRenderTreeList = ({ commit }, renderTreeList) => { export const setShowWhitespace = async ( { state, commit }, - { url, showWhitespace, updateDatabase = true }, + { url, showWhitespace, updateDatabase = true, trackClick = true }, ) => { if (updateDatabase && Boolean(window.gon?.current_user_id)) { await axios.put(url || state.endpointUpdateUser, { show_whitespace_in_diffs: showWhitespace }); @@ -587,7 +587,7 @@ export const setShowWhitespace = async ( commit(types.SET_SHOW_WHITESPACE, showWhitespace); notesEventHub.$emit('refetchDiffData'); - if (window.gon?.features?.diffSettingsUsageData) { + if (window.gon?.features?.diffSettingsUsageData && trackClick) { api.trackRedisHllUserEvent(TRACKING_CLICK_WHITESPACE_SETTING); if (showWhitespace) { diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue index a477aedd233..665e8ee69f7 100644 --- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue +++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue @@ -1,6 +1,6 @@ <script> import { GlButton, GlTooltipDirective } from '@gitlab/ui'; -import { mapState } from 'vuex'; +import { mapActions, mapState } from 'vuex'; export default { name: 'RemoveMemberButton', @@ -45,7 +45,7 @@ export default { oncallSchedules: { type: Object, required: false, - default: () => {}, + default: () => ({}), }, }, computed: { @@ -54,30 +54,35 @@ export default { return state[this.namespace].memberPath; }, }), - computedMemberPath() { - return this.memberPath.replace(':id', this.memberId); - }, - stringifiedSchedules() { - return JSON.stringify(this.oncallSchedules); + modalData() { + return { + isAccessRequest: this.isAccessRequest, + isInvite: this.isInvite, + memberPath: this.memberPath.replace(':id', this.memberId), + memberType: this.memberType, + message: this.message, + oncallSchedules: this.oncallSchedules, + }; }, }, + methods: { + ...mapActions({ + showRemoveMemberModal(dispatch, payload) { + return dispatch(`${this.namespace}/showRemoveMemberModal`, payload); + }, + }), + }, }; </script> <template> <gl-button - v-gl-tooltip.hover - class="js-remove-member-button" + v-gl-tooltip variant="danger" :title="title" :aria-label="title" :icon="icon" - :data-member-path="computedMemberPath" - :data-member-type="memberType" - :data-is-access-request="isAccessRequest" - :data-is-invite="isInvite" - :data-message="message" - :data-oncall-schedules="stringifiedSchedules" data-qa-selector="delete_member_button" + @click="showRemoveMemberModal(modalData)" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue b/app/assets/javascripts/members/components/modals/remove_member_modal.vue index 07272a5b8d6..00b6ebf9a73 100644 --- a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue +++ b/app/assets/javascripts/members/components/modals/remove_member_modal.vue @@ -1,7 +1,6 @@ <script> import { GlFormCheckbox, GlModal } from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; -import { parseBoolean } from '~/lib/utils/common_utils'; +import { mapActions, mapState } from 'vuex'; import csrf from '~/lib/utils/csrf'; import { s__, __ } from '~/locale'; import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue'; @@ -16,20 +15,33 @@ export default { GlModal, OncallSchedulesList, }, - data() { - return { - modalData: {}, - }; - }, + inject: ['namespace'], computed: { - isAccessRequest() { - return parseBoolean(this.modalData.isAccessRequest); - }, - isInvite() { - return parseBoolean(this.modalData.isInvite); - }, + ...mapState({ + isAccessRequest(state) { + return state[this.namespace].removeMemberModalData.isAccessRequest; + }, + isInvite(state) { + return state[this.namespace].removeMemberModalData.isInvite; + }, + memberPath(state) { + return state[this.namespace].removeMemberModalData.memberPath; + }, + memberType(state) { + return state[this.namespace].removeMemberModalData.memberType; + }, + message(state) { + return state[this.namespace].removeMemberModalData.message; + }, + oncallSchedules(state) { + return state[this.namespace].removeMemberModalData.oncallSchedules ?? {}; + }, + removeMemberModalVisible(state) { + return state[this.namespace].removeMemberModalVisible; + }, + }), isGroupMember() { - return this.modalData.memberType === 'GroupMember'; + return this.memberType === 'GroupMember'; }, actionText() { if (this.isAccessRequest) { @@ -54,29 +66,13 @@ export default { isPartOfOncallSchedules() { return !this.isAccessRequest && this.oncallSchedules.schedules?.length; }, - oncallSchedules() { - try { - return JSON.parse(this.modalData.oncallSchedules); - } catch (e) { - Sentry.captureException(e); - } - return {}; - }, - }, - mounted() { - document.addEventListener('click', this.handleClick); - }, - beforeDestroy() { - document.removeEventListener('click', this.handleClick); }, methods: { - handleClick(event) { - const removeButton = event.target.closest('.js-remove-member-button'); - if (removeButton) { - this.modalData = removeButton.dataset; - this.$refs.modal.show(); - } - }, + ...mapActions({ + hideRemoveMemberModal(dispatch) { + return dispatch(`${this.namespace}/hideRemoveMemberModal`); + }, + }), submitForm() { this.$refs.form.submit(); }, @@ -91,11 +87,13 @@ export default { :action-cancel="$options.actionCancel" :action-primary="actionPrimary" :title="actionText" + :visible="removeMemberModalVisible" data-qa-selector="remove_member_modal_content" @primary="submitForm" + @hide="hideRemoveMemberModal" > - <form ref="form" :action="modalData.memberPath" method="post"> - <p data-testid="modal-message">{{ modalData.message }}</p> + <form ref="form" :action="memberPath" method="post"> + <p>{{ message }}</p> <oncall-schedules-list v-if="isPartOfOncallSchedules" diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue index b9c80edbc49..debc3fc31f6 100644 --- a/app/assets/javascripts/members/components/table/members_table.vue +++ b/app/assets/javascripts/members/components/table/members_table.vue @@ -7,6 +7,7 @@ import { mergeUrlParams } from '~/lib/utils/url_utility'; import initUserPopovers from '~/user_popovers'; import { FIELDS, ACTIVE_TAB_QUERY_PARAM_NAME } from '../../constants'; import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue'; +import RemoveMemberModal from '../modals/remove_member_modal.vue'; import CreatedAt from './created_at.vue'; import ExpirationDatepicker from './expiration_datepicker.vue'; import ExpiresAt from './expires_at.vue'; @@ -29,6 +30,7 @@ export default { MemberActionButtons, RoleDropdown, RemoveGroupLinkModal, + RemoveMemberModal, ExpirationDatepicker, LdapOverrideConfirmationModal: () => import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'), @@ -225,6 +227,7 @@ export default { align="center" /> <remove-group-link-modal /> + <remove-member-modal /> <ldap-override-confirmation-modal /> </div> </template> diff --git a/app/assets/javascripts/members/store/actions.js b/app/assets/javascripts/members/store/actions.js index 7b191dd85d0..712f0d6caa7 100644 --- a/app/assets/javascripts/members/store/actions.js +++ b/app/assets/javascripts/members/store/actions.js @@ -25,6 +25,14 @@ export const hideRemoveGroupLinkModal = ({ commit }) => { commit(types.HIDE_REMOVE_GROUP_LINK_MODAL); }; +export const showRemoveMemberModal = ({ commit }, modalData) => { + commit(types.SHOW_REMOVE_MEMBER_MODAL, modalData); +}; + +export const hideRemoveMemberModal = ({ commit }) => { + commit(types.HIDE_REMOVE_MEMBER_MODAL); +}; + export const updateMemberExpiration = async ({ state, commit }, { memberId, expiresAt }) => { try { await axios.put( diff --git a/app/assets/javascripts/members/store/mutation_types.js b/app/assets/javascripts/members/store/mutation_types.js index 77307aa745b..5fa75725552 100644 --- a/app/assets/javascripts/members/store/mutation_types.js +++ b/app/assets/javascripts/members/store/mutation_types.js @@ -8,3 +8,6 @@ export const HIDE_ERROR = 'HIDE_ERROR'; export const SHOW_REMOVE_GROUP_LINK_MODAL = 'SHOW_REMOVE_GROUP_LINK_MODAL'; export const HIDE_REMOVE_GROUP_LINK_MODAL = 'HIDE_REMOVE_GROUP_LINK_MODAL'; + +export const SHOW_REMOVE_MEMBER_MODAL = 'SHOW_REMOVE_MEMBER_MODAL'; +export const HIDE_REMOVE_MEMBER_MODAL = 'HIDE_REMOVE_MEMBER_MODAL'; diff --git a/app/assets/javascripts/members/store/mutations.js b/app/assets/javascripts/members/store/mutations.js index f4aac1571d6..b4cf9f3480f 100644 --- a/app/assets/javascripts/members/store/mutations.js +++ b/app/assets/javascripts/members/store/mutations.js @@ -47,4 +47,11 @@ export default { [types.HIDE_REMOVE_GROUP_LINK_MODAL](state) { state.removeGroupLinkModalVisible = false; }, + [types.SHOW_REMOVE_MEMBER_MODAL](state, modalData) { + state.removeMemberModalData = modalData; + state.removeMemberModalVisible = true; + }, + [types.HIDE_REMOVE_MEMBER_MODAL](state) { + state.removeMemberModalVisible = false; + }, }; diff --git a/app/assets/javascripts/members/store/state.js b/app/assets/javascripts/members/store/state.js index 5415b1c5f25..c233a660840 100644 --- a/app/assets/javascripts/members/store/state.js +++ b/app/assets/javascripts/members/store/state.js @@ -20,4 +20,6 @@ export default ({ errorMessage: '', removeGroupLinkModalVisible: false, groupLinkToRemove: null, + removeMemberModalData: {}, + removeMemberModalVisible: false, }); diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js index 69d219d29f7..86b80a0ba5b 100644 --- a/app/assets/javascripts/pages/admin/groups/show/index.js +++ b/app/assets/javascripts/pages/admin/groups/show/index.js @@ -1,23 +1,3 @@ -import Vue from 'vue'; import UsersSelect from '~/users_select'; -import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; -function mountRemoveMemberModal() { - const el = document.querySelector('.js-remove-member-modal'); - if (!el) { - return false; - } - - return new Vue({ - el, - render(createComponent) { - return createComponent(RemoveMemberModal); - }, - }); -} - -document.addEventListener('DOMContentLoaded', () => { - mountRemoveMemberModal(); - - new UsersSelect(); // eslint-disable-line no-new -}); +new UsersSelect(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js index 042ff7808f1..b07ca815f13 100644 --- a/app/assets/javascripts/pages/admin/projects/index.js +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -1,23 +1,5 @@ -import Vue from 'vue'; import NamespaceSelect from '~/namespace_select'; import ProjectsList from '~/projects_list'; -import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; - -function mountRemoveMemberModal() { - const el = document.querySelector('.js-remove-member-modal'); - if (!el) { - return false; - } - - return new Vue({ - el, - render(createComponent) { - return createComponent(RemoveMemberModal); - }, - }); -} - -mountRemoveMemberModal(); new ProjectsList(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js index 13656ee9b16..0137ff87979 100644 --- a/app/assets/javascripts/pages/groups/group_members/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -1,4 +1,3 @@ -import Vue from 'vue'; import { groupMemberRequestFormatter } from '~/groups/members/utils'; import groupsSelect from '~/groups_select'; import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger'; @@ -11,21 +10,6 @@ import { initMembersApp } from '~/members'; import { MEMBER_TYPES } from '~/members/constants'; import { groupLinkRequestFormatter } from '~/members/utils'; import UsersSelect from '~/users_select'; -import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; - -function mountRemoveMemberModal() { - const el = document.querySelector('.js-remove-member-modal'); - if (!el) { - return false; - } - - return new Vue({ - el, - render(createComponent) { - return createComponent(RemoveMemberModal); - }, - }); -} const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions']; @@ -71,7 +55,6 @@ initMembersApp(document.querySelector('.js-group-members-list-app'), { groupsSelect(); memberExpirationDate(); memberExpirationDate('.js-access-expiration-date-groups'); -mountRemoveMemberModal(); initInviteMembersModal(); initInviteMembersTrigger(); initInviteGroupTrigger(); diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js index 177dc346c60..fb0be31834d 100644 --- a/app/assets/javascripts/pages/projects/project_members/index.js +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -1,4 +1,3 @@ -import Vue from 'vue'; import groupsSelect from '~/groups_select'; import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger'; import initInviteMembersForm from '~/invite_members/init_invite_members_form'; @@ -11,26 +10,10 @@ import { MEMBER_TYPES } from '~/members/constants'; import { groupLinkRequestFormatter } from '~/members/utils'; import { projectMemberRequestFormatter } from '~/projects/members/utils'; import UsersSelect from '~/users_select'; -import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; - -function mountRemoveMemberModal() { - const el = document.querySelector('.js-remove-member-modal'); - if (!el) { - return false; - } - - return new Vue({ - el, - render(createComponent) { - return createComponent(RemoveMemberModal); - }, - }); -} groupsSelect(); memberExpirationDate(); memberExpirationDate('.js-access-expiration-date-groups'); -mountRemoveMemberModal(); initInviteMembersModal(); initInviteMembersTrigger(); initInviteGroupTrigger(); diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue index 85d14547efd..a5bc1680852 100644 --- a/app/assets/javascripts/runner/components/runner_update_form.vue +++ b/app/assets/javascripts/runner/components/runner_update_form.vue @@ -111,7 +111,7 @@ export default { > {{ __('Paused') }} <template #help> - {{ __("Paused runners don't accept new jobs") }} + {{ s__('Runners|Stop the runner from accepting new jobs.') }} </template> </gl-form-checkbox> @@ -123,14 +123,14 @@ export default { > {{ __('Protected') }} <template #help> - {{ __('This runner will only run on pipelines triggered on protected branches') }} + {{ s__('Runners|Use the runner on pipelines for protected branches only.') }} </template> </gl-form-checkbox> <gl-form-checkbox v-model="model.runUntagged" data-testid="runner-field-run-untagged"> {{ __('Run untagged jobs') }} <template #help> - {{ __('Indicates whether this runner can pick jobs without tags') }} + {{ s__('Runners|Use the runner for jobs without tags, in addition to tagged jobs.') }} </template> </gl-form-checkbox> @@ -141,7 +141,7 @@ export default { > {{ __('Lock to current projects') }} <template #help> - {{ __('When a runner is locked, it cannot be assigned to other projects') }} + {{ s__('Runners|Use the runner for the currently assigned projects only.') }} </template> </gl-form-checkbox> diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js index 5cb9277040d..d80c67da8b1 100644 --- a/app/assets/javascripts/security_configuration/components/constants.js +++ b/app/assets/javascripts/security_configuration/components/constants.js @@ -98,6 +98,10 @@ export const COVERAGE_FUZZING_DESCRIPTION = __( export const COVERAGE_FUZZING_HELP_PATH = helpPagePath( 'user/application_security/coverage_fuzzing/index', ); +export const COVERAGE_FUZZING_CONFIG_HELP_PATH = helpPagePath( + 'user/application_security/coverage_fuzzing/index', + { anchor: 'configuration' }, +); export const API_FUZZING_NAME = __('API Fuzzing'); export const API_FUZZING_DESCRIPTION = __('Find bugs in your code with API fuzzing.'); @@ -262,6 +266,7 @@ export const securityFeatures = [ name: COVERAGE_FUZZING_NAME, description: COVERAGE_FUZZING_DESCRIPTION, helpPath: COVERAGE_FUZZING_HELP_PATH, + configurationHelpPath: COVERAGE_FUZZING_CONFIG_HELP_PATH, type: REPORT_TYPE_COVERAGE_FUZZING, }, ]; diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue index 23cffde1f83..6ca96504982 100644 --- a/app/assets/javascripts/security_configuration/components/feature_card.vue +++ b/app/assets/javascripts/security_configuration/components/feature_card.vue @@ -125,7 +125,12 @@ export default { class="gl-mt-5" /> - <gl-button v-else icon="external-link" :href="feature.configurationHelpPath" class="gl-mt-5"> + <gl-button + v-else-if="feature.configurationHelpPath" + icon="external-link" + :href="feature.configurationHelpPath" + class="gl-mt-5" + > {{ $options.i18n.configurationGuide }} </gl-button> </template> diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 9f1e2d8236a..ecc5ad1f84e 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -10,10 +10,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path] before_action :build_merge_request, except: [:create] - before_action do - push_frontend_feature_flag(:mr_collapsed_approval_rules, @project) - end - def new define_new_vars end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index cfa64bbc16d..8c69fe61b67 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -55,7 +55,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end before_action do - push_frontend_feature_flag(:mr_collapsed_approval_rules, @project) push_frontend_feature_flag(:show_relevant_approval_rule_approvers, @project, default_enabled: :yaml) end diff --git a/app/graphql/resolvers/alert_management/alert_resolver.rb b/app/graphql/resolvers/alert_management/alert_resolver.rb index 62744e719da..899b90a94dd 100644 --- a/app/graphql/resolvers/alert_management/alert_resolver.rb +++ b/app/graphql/resolvers/alert_management/alert_resolver.rb @@ -5,7 +5,7 @@ module Resolvers class AlertResolver < BaseResolver include LooksAhead - argument :iid, GraphQL::STRING_TYPE, + argument :iid, GraphQL::Types::String, required: false, description: 'IID of the alert. For example, "1".' @@ -23,11 +23,11 @@ module Resolvers required: true, default_value: 'operations' - argument :search, GraphQL::STRING_TYPE, + argument :search, GraphQL::Types::String, description: 'Search query for title, description, service, or monitoring_tool.', required: false - argument :assignee_username, GraphQL::STRING_TYPE, + argument :assignee_username, GraphQL::Types::String, required: false, description: 'Username of a user assigned to the issue.' diff --git a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb index 410f72cff84..568d648a95e 100644 --- a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb +++ b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb @@ -5,11 +5,11 @@ module Resolvers class AlertStatusCountsResolver < BaseResolver type Types::AlertManagement::AlertStatusCountsType, null: true - argument :search, GraphQL::STRING_TYPE, + argument :search, GraphQL::Types::String, description: 'Search query for title, description, service, or monitoring_tool.', required: false - argument :assignee_username, GraphQL::STRING_TYPE, + argument :assignee_username, GraphQL::Types::String, required: false, description: 'Username of a user assigned to the issue.' diff --git a/app/graphql/resolvers/blobs_resolver.rb b/app/graphql/resolvers/blobs_resolver.rb index d006769bd4b..4c35f6c342e 100644 --- a/app/graphql/resolvers/blobs_resolver.rb +++ b/app/graphql/resolvers/blobs_resolver.rb @@ -10,10 +10,10 @@ module Resolvers alias_method :repository, :object - argument :paths, [GraphQL::STRING_TYPE], + argument :paths, [GraphQL::Types::String], required: true, description: 'Array of desired blob paths.' - argument :ref, GraphQL::STRING_TYPE, + argument :ref, GraphQL::Types::String, required: false, default_value: nil, description: 'The commit ref to get the blobs from. Default value is HEAD.' diff --git a/app/graphql/resolvers/ci/config_resolver.rb b/app/graphql/resolvers/ci/config_resolver.rb index f2e33251b9c..eb331331c70 100644 --- a/app/graphql/resolvers/ci/config_resolver.rb +++ b/app/graphql/resolvers/ci/config_resolver.rb @@ -14,19 +14,19 @@ module Resolvers authorize :read_pipeline - argument :project_path, GraphQL::ID_TYPE, + argument :project_path, GraphQL::Types::ID, required: true, description: 'The project of the CI config.' - argument :sha, GraphQL::STRING_TYPE, + argument :sha, GraphQL::Types::String, required: false, description: "Sha for the pipeline." - argument :content, GraphQL::STRING_TYPE, + argument :content, GraphQL::Types::String, required: true, description: "Contents of `.gitlab-ci.yml`." - argument :dry_run, GraphQL::BOOLEAN_TYPE, + argument :dry_run, GraphQL::Types::Boolean, required: false, description: 'Run pipeline creation simulation, or only do static check.' diff --git a/app/graphql/resolvers/ci/runner_setup_resolver.rb b/app/graphql/resolvers/ci/runner_setup_resolver.rb index 9166999b400..6b21f5c3ab4 100644 --- a/app/graphql/resolvers/ci/runner_setup_resolver.rb +++ b/app/graphql/resolvers/ci/runner_setup_resolver.rb @@ -9,12 +9,12 @@ module Resolvers description 'Runner setup instructions.' argument :platform, - type: GraphQL::STRING_TYPE, + type: GraphQL::Types::String, required: true, description: 'Platform to generate the instructions for.' argument :architecture, - type: GraphQL::STRING_TYPE, + type: GraphQL::Types::String, required: true, description: 'Architecture to generate the instructions for.' diff --git a/app/graphql/resolvers/ci/runners_resolver.rb b/app/graphql/resolvers/ci/runners_resolver.rb index 5074a248e18..1957c4ec058 100644 --- a/app/graphql/resolvers/ci/runners_resolver.rb +++ b/app/graphql/resolvers/ci/runners_resolver.rb @@ -15,11 +15,11 @@ module Resolvers required: false, description: 'Filter runners by type.' - argument :tag_list, [GraphQL::STRING_TYPE], + argument :tag_list, [GraphQL::Types::String], required: false, description: 'Filter by tags associated with the runner (comma-separated or array).' - argument :search, GraphQL::STRING_TYPE, + argument :search, GraphQL::Types::String, required: false, description: 'Filter by full token or partial text in description field.' diff --git a/app/graphql/resolvers/ci/template_resolver.rb b/app/graphql/resolvers/ci/template_resolver.rb index 7f5a1a486d7..17f2668df11 100644 --- a/app/graphql/resolvers/ci/template_resolver.rb +++ b/app/graphql/resolvers/ci/template_resolver.rb @@ -5,7 +5,7 @@ module Resolvers class TemplateResolver < BaseResolver type Types::Ci::TemplateType, null: true - argument :name, GraphQL::STRING_TYPE, required: true, + argument :name, GraphQL::Types::String, required: true, description: 'Name of the CI/CD template to search for. Template must be formatted as `Name.gitlab-ci.yml`.' alias_method :project, :object diff --git a/app/graphql/resolvers/ci/test_suite_resolver.rb b/app/graphql/resolvers/ci/test_suite_resolver.rb index 90cc30b1281..5d61d9e986b 100644 --- a/app/graphql/resolvers/ci/test_suite_resolver.rb +++ b/app/graphql/resolvers/ci/test_suite_resolver.rb @@ -11,7 +11,7 @@ module Resolvers alias_method :pipeline, :object - argument :build_ids, [GraphQL::ID_TYPE], + argument :build_ids, [GraphQL::Types::ID], required: true, description: 'IDs of the builds used to run the test suite.' diff --git a/app/graphql/resolvers/concerns/group_issuable_resolver.rb b/app/graphql/resolvers/concerns/group_issuable_resolver.rb index 49a79683e9f..542ff5374ff 100644 --- a/app/graphql/resolvers/concerns/group_issuable_resolver.rb +++ b/app/graphql/resolvers/concerns/group_issuable_resolver.rb @@ -5,7 +5,7 @@ module GroupIssuableResolver class_methods do def include_subgroups(name_of_things) - argument :include_subgroups, GraphQL::BOOLEAN_TYPE, + argument :include_subgroups, GraphQL::Types::Boolean, required: false, default_value: false, description: "Include #{name_of_things} belonging to subgroups" diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb index c24f4dedc0e..b91dca3fc47 100644 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb @@ -6,29 +6,29 @@ module IssueResolverArguments prepended do include LooksAhead - argument :iid, GraphQL::STRING_TYPE, + argument :iid, GraphQL::Types::String, required: false, description: 'IID of the issue. For example, "1".' - argument :iids, [GraphQL::STRING_TYPE], + argument :iids, [GraphQL::Types::String], required: false, description: 'List of IIDs of issues. For example, `["1", "2"]`.' - argument :label_name, [GraphQL::STRING_TYPE, null: true], + argument :label_name, [GraphQL::Types::String, null: true], required: false, description: 'Labels applied to this issue.' - argument :milestone_title, [GraphQL::STRING_TYPE, null: true], + argument :milestone_title, [GraphQL::Types::String, null: true], required: false, description: 'Milestone applied to this issue.' - argument :author_username, GraphQL::STRING_TYPE, + argument :author_username, GraphQL::Types::String, required: false, description: 'Username of the author of the issue.' - argument :assignee_username, GraphQL::STRING_TYPE, + argument :assignee_username, GraphQL::Types::String, required: false, description: 'Username of a user assigned to the issue.', deprecated: { reason: 'Use `assigneeUsernames`', milestone: '13.11' } - argument :assignee_usernames, [GraphQL::STRING_TYPE], + argument :assignee_usernames, [GraphQL::Types::String], required: false, description: 'Usernames of users assigned to the issue.' - argument :assignee_id, GraphQL::STRING_TYPE, + argument :assignee_id, GraphQL::Types::String, required: false, description: 'ID of a user assigned to the issues, "none" and "any" values are supported.' argument :created_before, Types::TimeType, @@ -49,7 +49,7 @@ module IssueResolverArguments argument :closed_after, Types::TimeType, required: false, description: 'Issues closed after this date.' - argument :search, GraphQL::STRING_TYPE, + argument :search, GraphQL::Types::String, required: false, description: 'Search query for issue title or description.' argument :types, [Types::IssueTypeEnum], diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb index bbf33c0b5eb..77f2105db7c 100644 --- a/app/graphql/resolvers/concerns/resolves_pipelines.rb +++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb @@ -10,11 +10,11 @@ module ResolvesPipelines required: false, description: "Filter pipelines by their status." argument :ref, - GraphQL::STRING_TYPE, + GraphQL::Types::String, required: false, description: "Filter pipelines by the ref they are run for." argument :sha, - GraphQL::STRING_TYPE, + GraphQL::Types::String, required: false, description: "Filter pipelines by the sha of the commit they are run for." end diff --git a/app/graphql/resolvers/container_repositories_resolver.rb b/app/graphql/resolvers/container_repositories_resolver.rb index 17af2a2f070..1e06d4e9094 100644 --- a/app/graphql/resolvers/container_repositories_resolver.rb +++ b/app/graphql/resolvers/container_repositories_resolver.rb @@ -6,7 +6,7 @@ module Resolvers type Types::ContainerRepositoryType, null: true - argument :name, GraphQL::STRING_TYPE, + argument :name, GraphQL::Types::String, required: false, description: 'Filter the container repositories by their name.' diff --git a/app/graphql/resolvers/design_management/design_resolver.rb b/app/graphql/resolvers/design_management/design_resolver.rb index e640f57f04a..d9e5203ef0e 100644 --- a/app/graphql/resolvers/design_management/design_resolver.rb +++ b/app/graphql/resolvers/design_management/design_resolver.rb @@ -11,7 +11,7 @@ module Resolvers required: false, description: 'Find a design by its ID.' - argument :filename, GraphQL::STRING_TYPE, + argument :filename, GraphQL::Types::String, required: false, description: 'Find a design by its filename.' diff --git a/app/graphql/resolvers/design_management/designs_resolver.rb b/app/graphql/resolvers/design_management/designs_resolver.rb index c45e4d01f6e..dec778fac80 100644 --- a/app/graphql/resolvers/design_management/designs_resolver.rb +++ b/app/graphql/resolvers/design_management/designs_resolver.rb @@ -11,7 +11,7 @@ module Resolvers argument :ids, [DesignID], required: false, description: 'Filters designs by their ID.' - argument :filenames, [GraphQL::STRING_TYPE], + argument :filenames, [GraphQL::Types::String], required: false, description: 'Filters designs by their filename.' argument :at_version, VersionID, diff --git a/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb index fea74cbfb0b..3f872ebbc55 100644 --- a/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb +++ b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb @@ -23,7 +23,7 @@ module Resolvers argument :design_id, DesignID, required: false, description: 'The ID of a specific design.' - argument :filename, GraphQL::STRING_TYPE, + argument :filename, GraphQL::Types::String, required: false, description: 'The filename of a specific design.' diff --git a/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb index 930b1b60d0e..254f1efa0a5 100644 --- a/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb +++ b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb @@ -17,7 +17,7 @@ module Resolvers required: false, description: 'Filters designs by their ID.' argument :filenames, - [GraphQL::STRING_TYPE], + [GraphQL::Types::String], required: false, description: 'Filters designs by their filename.' diff --git a/app/graphql/resolvers/design_management/version_in_collection_resolver.rb b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb index 593974beb04..10c3d4126d4 100644 --- a/app/graphql/resolvers/design_management/version_in_collection_resolver.rb +++ b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb @@ -15,7 +15,7 @@ module Resolvers VersionID = ::Types::GlobalIDType[::DesignManagement::Version] - argument :sha, GraphQL::STRING_TYPE, + argument :sha, GraphQL::Types::String, required: false, description: "The SHA256 of a specific version." argument :id, VersionID, diff --git a/app/graphql/resolvers/design_management/versions_resolver.rb b/app/graphql/resolvers/design_management/versions_resolver.rb index 08b29d884b0..71ab768c89e 100644 --- a/app/graphql/resolvers/design_management/versions_resolver.rb +++ b/app/graphql/resolvers/design_management/versions_resolver.rb @@ -11,7 +11,7 @@ module Resolvers extras [:parent] - argument :earlier_or_equal_to_sha, GraphQL::STRING_TYPE, + argument :earlier_or_equal_to_sha, GraphQL::Types::String, as: :sha, required: false, description: 'The SHA256 of the most recent acceptable version.' diff --git a/app/graphql/resolvers/echo_resolver.rb b/app/graphql/resolvers/echo_resolver.rb index a09b0a1fd87..856e6873a65 100644 --- a/app/graphql/resolvers/echo_resolver.rb +++ b/app/graphql/resolvers/echo_resolver.rb @@ -2,11 +2,11 @@ module Resolvers class EchoResolver < BaseResolver - type ::GraphQL::STRING_TYPE, null: false + type ::GraphQL::Types::String, null: false description 'Testing endpoint to validate the API with' argument :text, - type: GraphQL::STRING_TYPE, + type: GraphQL::Types::String, required: true, description: 'Text to echo back.' diff --git a/app/graphql/resolvers/environments_resolver.rb b/app/graphql/resolvers/environments_resolver.rb index ee604e7b307..1823eb65d44 100644 --- a/app/graphql/resolvers/environments_resolver.rb +++ b/app/graphql/resolvers/environments_resolver.rb @@ -2,15 +2,15 @@ module Resolvers class EnvironmentsResolver < BaseResolver - argument :name, GraphQL::STRING_TYPE, + argument :name, GraphQL::Types::String, required: false, description: 'Name of the environment.' - argument :search, GraphQL::STRING_TYPE, + argument :search, GraphQL::Types::String, required: false, description: 'Search query for environment name.' - argument :states, [GraphQL::STRING_TYPE], + argument :states, [GraphQL::Types::String], required: false, description: 'States of environments that should be included in result.' diff --git a/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb index 4cd65daa655..793b73342ab 100644 --- a/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb +++ b/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb @@ -6,12 +6,12 @@ module Resolvers type Types::ErrorTracking::SentryErrorType.connection_type, null: true extension Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension - argument :search_term, ::GraphQL::STRING_TYPE, + argument :search_term, ::GraphQL::Types::String, description: 'Search query for the Sentry error details.', required: false # TODO: convert to Enum - argument :sort, ::GraphQL::STRING_TYPE, + argument :sort, ::GraphQL::Types::String, description: 'Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.', required: false diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb index b5e90da78b2..2dab55a1954 100644 --- a/app/graphql/resolvers/full_path_resolver.rb +++ b/app/graphql/resolvers/full_path_resolver.rb @@ -5,7 +5,7 @@ module Resolvers extend ActiveSupport::Concern prepended do - argument :full_path, GraphQL::ID_TYPE, + argument :full_path, GraphQL::Types::ID, required: true, description: 'The full path of the project, group or namespace, e.g., `gitlab-org/gitlab-foss`.' end diff --git a/app/graphql/resolvers/group_labels_resolver.rb b/app/graphql/resolvers/group_labels_resolver.rb index 5c2f950bbc0..a22fa9761d6 100644 --- a/app/graphql/resolvers/group_labels_resolver.rb +++ b/app/graphql/resolvers/group_labels_resolver.rb @@ -4,12 +4,12 @@ module Resolvers class GroupLabelsResolver < LabelsResolver type Types::LabelType.connection_type, null: true - argument :include_descendant_groups, GraphQL::BOOLEAN_TYPE, + argument :include_descendant_groups, GraphQL::Types::Boolean, required: false, description: 'Include labels from descendant groups.', default_value: false - argument :only_group_labels, GraphQL::BOOLEAN_TYPE, + argument :only_group_labels, GraphQL::Types::Boolean, required: false, description: 'Include only group level labels.', default_value: false diff --git a/app/graphql/resolvers/group_milestones_resolver.rb b/app/graphql/resolvers/group_milestones_resolver.rb index 31280b36278..44192b6f4bf 100644 --- a/app/graphql/resolvers/group_milestones_resolver.rb +++ b/app/graphql/resolvers/group_milestones_resolver.rb @@ -2,10 +2,10 @@ module Resolvers class GroupMilestonesResolver < MilestonesResolver - argument :include_descendants, GraphQL::BOOLEAN_TYPE, + argument :include_descendants, GraphQL::Types::Boolean, required: false, description: 'Include milestones from all subgroups and subprojects.' - argument :include_ancestors, GraphQL::BOOLEAN_TYPE, + argument :include_ancestors, GraphQL::Types::Boolean, required: false, description: 'Include milestones from all parent groups.' diff --git a/app/graphql/resolvers/labels_resolver.rb b/app/graphql/resolvers/labels_resolver.rb index 1b523b8a240..505d1dff8d2 100644 --- a/app/graphql/resolvers/labels_resolver.rb +++ b/app/graphql/resolvers/labels_resolver.rb @@ -8,11 +8,11 @@ module Resolvers type Types::LabelType.connection_type, null: true - argument :search_term, GraphQL::STRING_TYPE, + argument :search_term, GraphQL::Types::String, required: false, description: 'A search term to find labels with.' - argument :include_ancestor_groups, GraphQL::BOOLEAN_TYPE, + argument :include_ancestor_groups, GraphQL::Types::Boolean, required: false, description: 'Include labels from ancestor groups.', default_value: false diff --git a/app/graphql/resolvers/members_resolver.rb b/app/graphql/resolvers/members_resolver.rb index 2b731d54cdd..827db54134a 100644 --- a/app/graphql/resolvers/members_resolver.rb +++ b/app/graphql/resolvers/members_resolver.rb @@ -7,7 +7,7 @@ module Resolvers type Types::MemberInterface.connection_type, null: true - argument :search, GraphQL::STRING_TYPE, + argument :search, GraphQL::Types::String, required: false, description: 'Search query.' diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb index c431d079beb..0f0f3ee3772 100644 --- a/app/graphql/resolvers/merge_request_resolver.rb +++ b/app/graphql/resolvers/merge_request_resolver.rb @@ -8,7 +8,7 @@ module Resolvers type ::Types::MergeRequestType, null: true - argument :iid, GraphQL::STRING_TYPE, + argument :iid, GraphQL::Types::String, required: true, as: :iids, description: 'IID of the merge request, for example `1`.' diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb index a9eea4ae4b8..8f2c7847a2e 100644 --- a/app/graphql/resolvers/merge_requests_resolver.rb +++ b/app/graphql/resolvers/merge_requests_resolver.rb @@ -10,28 +10,28 @@ module Resolvers alias_method :project, :object def self.accept_assignee - argument :assignee_username, GraphQL::STRING_TYPE, + argument :assignee_username, GraphQL::Types::String, required: false, description: 'Username of the assignee.' end def self.accept_author - argument :author_username, GraphQL::STRING_TYPE, + argument :author_username, GraphQL::Types::String, required: false, description: 'Username of the author.' end def self.accept_reviewer - argument :reviewer_username, GraphQL::STRING_TYPE, + argument :reviewer_username, GraphQL::Types::String, required: false, description: 'Username of the reviewer.' end - argument :iids, [GraphQL::STRING_TYPE], + argument :iids, [GraphQL::Types::String], required: false, description: 'Array of IIDs of merge requests, for example `[1, 2]`.' - argument :source_branches, [GraphQL::STRING_TYPE], + argument :source_branches, [GraphQL::Types::String], required: false, as: :source_branch, description: <<~DESC @@ -39,7 +39,7 @@ module Resolvers All resolved merge requests will have one of these branches as their source. DESC - argument :target_branches, [GraphQL::STRING_TYPE], + argument :target_branches, [GraphQL::Types::String], required: false, as: :target_branch, description: <<~DESC @@ -51,7 +51,7 @@ module Resolvers required: false, description: 'A merge request state. If provided, all resolved merge requests will have this state.' - argument :labels, [GraphQL::STRING_TYPE], + argument :labels, [GraphQL::Types::String], required: false, as: :label_name, description: 'Array of label names. All resolved merge requests will have all of these labels.' @@ -61,7 +61,7 @@ module Resolvers argument :merged_before, Types::TimeType, required: false, description: 'Merge requests merged before this date.' - argument :milestone_title, GraphQL::STRING_TYPE, + argument :milestone_title, GraphQL::Types::String, required: false, description: 'Title of the milestone.' argument :sort, Types::MergeRequestSortEnum, @@ -70,11 +70,11 @@ module Resolvers default_value: :created_desc negated do - argument :labels, [GraphQL::STRING_TYPE], + argument :labels, [GraphQL::Types::String], required: false, as: :label_name, description: 'Array of label names. All resolved merge requests will not have these labels.' - argument :milestone_title, GraphQL::STRING_TYPE, + argument :milestone_title, GraphQL::Types::String, required: false, description: 'Title of the milestone.' end diff --git a/app/graphql/resolvers/metrics/dashboard_resolver.rb b/app/graphql/resolvers/metrics/dashboard_resolver.rb index 0669fececd5..d2be9fcdd89 100644 --- a/app/graphql/resolvers/metrics/dashboard_resolver.rb +++ b/app/graphql/resolvers/metrics/dashboard_resolver.rb @@ -6,7 +6,7 @@ module Resolvers type Types::Metrics::DashboardType, null: true calls_gitaly! - argument :path, GraphQL::STRING_TYPE, + argument :path, GraphQL::Types::String, required: true, description: <<~MD Path to a file which defines a metrics dashboard eg: `"config/prometheus/common_metrics.yml"`. diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb index 4fa4c939a23..84f7d66ec19 100644 --- a/app/graphql/resolvers/milestones_resolver.rb +++ b/app/graphql/resolvers/milestones_resolver.rb @@ -5,7 +5,7 @@ module Resolvers include Gitlab::Graphql::Authorize::AuthorizeResource include TimeFrameArguments - argument :ids, [GraphQL::ID_TYPE], + argument :ids, [GraphQL::Types::ID], required: false, description: 'Array of global milestone IDs, e.g., `"gid://gitlab/Milestone/1"`.' @@ -13,11 +13,11 @@ module Resolvers required: false, description: 'Filter milestones by state.' - argument :title, GraphQL::STRING_TYPE, + argument :title, GraphQL::Types::String, required: false, description: 'The title of the milestone.' - argument :search_title, GraphQL::STRING_TYPE, + argument :search_title, GraphQL::Types::String, required: false, description: 'A search string for the title.' diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb index 86286a744bd..c3c61d31e8d 100644 --- a/app/graphql/resolvers/namespace_projects_resolver.rb +++ b/app/graphql/resolvers/namespace_projects_resolver.rb @@ -2,12 +2,12 @@ module Resolvers class NamespaceProjectsResolver < BaseResolver - argument :include_subgroups, GraphQL::BOOLEAN_TYPE, + argument :include_subgroups, GraphQL::Types::Boolean, required: false, default_value: false, description: 'Include also subgroup projects.' - argument :search, GraphQL::STRING_TYPE, + argument :search, GraphQL::Types::String, required: false, default_value: nil, description: 'Search project with most similar names or paths.' @@ -17,7 +17,7 @@ module Resolvers default_value: nil, description: 'Sort projects by this criteria.' - argument :ids, [GraphQL::ID_TYPE], + argument :ids, [GraphQL::Types::ID], required: false, default_value: nil, description: 'Filter projects by IDs.' diff --git a/app/graphql/resolvers/packages_base_resolver.rb b/app/graphql/resolvers/packages_base_resolver.rb index 3378cc32c9f..7d153d16910 100644 --- a/app/graphql/resolvers/packages_base_resolver.rb +++ b/app/graphql/resolvers/packages_base_resolver.rb @@ -9,7 +9,7 @@ module Resolvers required: false, default_value: :created_desc - argument :package_name, GraphQL::STRING_TYPE, + argument :package_name, GraphQL::Types::String, description: 'Search a package by name.', required: false, default_value: nil @@ -24,7 +24,7 @@ module Resolvers required: false, default_value: nil - argument :include_versionless, GraphQL::BOOLEAN_TYPE, + argument :include_versionless, GraphQL::Types::Boolean, description: 'Include versionless packages.', required: false, default_value: false diff --git a/app/graphql/resolvers/project_milestones_resolver.rb b/app/graphql/resolvers/project_milestones_resolver.rb index 4cd9cb53f56..567a55aa09b 100644 --- a/app/graphql/resolvers/project_milestones_resolver.rb +++ b/app/graphql/resolvers/project_milestones_resolver.rb @@ -3,7 +3,7 @@ module Resolvers class ProjectMilestonesResolver < MilestonesResolver - argument :include_ancestors, GraphQL::BOOLEAN_TYPE, + argument :include_ancestors, GraphQL::Types::Boolean, required: false, description: "Also return milestones in the project's parent group and its ancestors." diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb index aa8808b15ac..ce4b6ac6b0c 100644 --- a/app/graphql/resolvers/project_pipeline_resolver.rb +++ b/app/graphql/resolvers/project_pipeline_resolver.rb @@ -6,11 +6,11 @@ module Resolvers alias_method :project, :object - argument :iid, GraphQL::ID_TYPE, + argument :iid, GraphQL::Types::ID, required: false, description: 'IID of the Pipeline. For example, "1".' - argument :sha, GraphQL::STRING_TYPE, + argument :sha, GraphQL::Types::String, required: false, description: 'SHA of the Pipeline. For example, "dyd0f15ay83993f5ab66k927w28673882x99100b".' diff --git a/app/graphql/resolvers/projects/jira_projects_resolver.rb b/app/graphql/resolvers/projects/jira_projects_resolver.rb index 864acb6d759..56432585015 100644 --- a/app/graphql/resolvers/projects/jira_projects_resolver.rb +++ b/app/graphql/resolvers/projects/jira_projects_resolver.rb @@ -9,7 +9,7 @@ module Resolvers authorize :admin_project argument :name, - GraphQL::STRING_TYPE, + GraphQL::Types::String, required: false, description: 'Project name or key.' diff --git a/app/graphql/resolvers/projects/services_resolver.rb b/app/graphql/resolvers/projects/services_resolver.rb index 3674aa97e1f..99de4df945c 100644 --- a/app/graphql/resolvers/projects/services_resolver.rb +++ b/app/graphql/resolvers/projects/services_resolver.rb @@ -10,7 +10,7 @@ module Resolvers authorizes_object! argument :active, - GraphQL::BOOLEAN_TYPE, + GraphQL::Types::Boolean, required: false, description: 'Indicates if the integration is active.' argument :type, diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb index 665ec796cd3..b846248458f 100644 --- a/app/graphql/resolvers/projects_resolver.rb +++ b/app/graphql/resolvers/projects_resolver.rb @@ -4,27 +4,27 @@ module Resolvers class ProjectsResolver < BaseResolver type Types::ProjectType, null: true - argument :membership, GraphQL::BOOLEAN_TYPE, + argument :membership, GraphQL::Types::Boolean, required: false, description: 'Limit projects that the current user is a member of.' - argument :search, GraphQL::STRING_TYPE, + argument :search, GraphQL::Types::String, required: false, description: 'Search query for project name, path, or description.' - argument :ids, [GraphQL::ID_TYPE], + argument :ids, [GraphQL::Types::ID], required: false, description: 'Filter projects by IDs.' - argument :search_namespaces, GraphQL::BOOLEAN_TYPE, + argument :search_namespaces, GraphQL::Types::Boolean, required: false, description: 'Include namespace in project search.' - argument :sort, GraphQL::STRING_TYPE, + argument :sort, GraphQL::Types::String, required: false, description: 'Sort order of results.' - argument :topics, type: [GraphQL::STRING_TYPE], + argument :topics, type: [GraphQL::Types::String], required: false, description: 'Filters projects by topics.' diff --git a/app/graphql/resolvers/release_resolver.rb b/app/graphql/resolvers/release_resolver.rb index 67ff5fed0bb..0374a1103de 100644 --- a/app/graphql/resolvers/release_resolver.rb +++ b/app/graphql/resolvers/release_resolver.rb @@ -4,7 +4,7 @@ module Resolvers class ReleaseResolver < BaseResolver type Types::ReleaseType, null: true - argument :tag_name, GraphQL::STRING_TYPE, + argument :tag_name, GraphQL::Types::String, required: true, description: 'The name of the tag associated to the release.' diff --git a/app/graphql/resolvers/repository_branch_names_resolver.rb b/app/graphql/resolvers/repository_branch_names_resolver.rb index c0a5ea0366f..e9aacda2652 100644 --- a/app/graphql/resolvers/repository_branch_names_resolver.rb +++ b/app/graphql/resolvers/repository_branch_names_resolver.rb @@ -2,19 +2,19 @@ module Resolvers class RepositoryBranchNamesResolver < BaseResolver - type ::GraphQL::STRING_TYPE, null: false + type ::GraphQL::Types::String, null: false calls_gitaly! - argument :search_pattern, GraphQL::STRING_TYPE, + argument :search_pattern, GraphQL::Types::String, required: true, description: 'The pattern to search for branch names by.' - argument :offset, GraphQL::INT_TYPE, + argument :offset, GraphQL::Types::Int, required: true, description: 'The number of branch names to skip.' - argument :limit, GraphQL::INT_TYPE, + argument :limit, GraphQL::Types::Int, required: true, description: 'The number of branch names to return.' diff --git a/app/graphql/resolvers/snippets/blobs_resolver.rb b/app/graphql/resolvers/snippets/blobs_resolver.rb index 4328d38d485..00f41517422 100644 --- a/app/graphql/resolvers/snippets/blobs_resolver.rb +++ b/app/graphql/resolvers/snippets/blobs_resolver.rb @@ -12,7 +12,7 @@ module Resolvers alias_method :snippet, :object - argument :paths, [GraphQL::STRING_TYPE], + argument :paths, [GraphQL::Types::String], required: false, description: 'Paths of the blobs.' diff --git a/app/graphql/resolvers/snippets_resolver.rb b/app/graphql/resolvers/snippets_resolver.rb index 7153c919062..4e34185b189 100644 --- a/app/graphql/resolvers/snippets_resolver.rb +++ b/app/graphql/resolvers/snippets_resolver.rb @@ -22,7 +22,7 @@ module Resolvers description: 'The type of snippet.' argument :explore, - GraphQL::BOOLEAN_TYPE, + GraphQL::Types::Boolean, required: false, description: 'Explore personal snippets.' diff --git a/app/graphql/resolvers/terraform/states_resolver.rb b/app/graphql/resolvers/terraform/states_resolver.rb index f543eb182e8..69785cfc794 100644 --- a/app/graphql/resolvers/terraform/states_resolver.rb +++ b/app/graphql/resolvers/terraform/states_resolver.rb @@ -8,7 +8,7 @@ module Resolvers alias_method :project, :object when_single do - argument :name, GraphQL::STRING_TYPE, + argument :name, GraphQL::Types::String, required: true, description: 'Name of the Terraform state.' end diff --git a/app/graphql/resolvers/todo_resolver.rb b/app/graphql/resolvers/todo_resolver.rb index 8966285fccc..9de61e95898 100644 --- a/app/graphql/resolvers/todo_resolver.rb +++ b/app/graphql/resolvers/todo_resolver.rb @@ -10,15 +10,15 @@ module Resolvers required: false, description: 'The action to be filtered.' - argument :author_id, [GraphQL::ID_TYPE], + argument :author_id, [GraphQL::Types::ID], required: false, description: 'The ID of an author.' - argument :project_id, [GraphQL::ID_TYPE], + argument :project_id, [GraphQL::Types::ID], required: false, description: 'The ID of a project.' - argument :group_id, [GraphQL::ID_TYPE], + argument :group_id, [GraphQL::Types::ID], required: false, description: 'The ID of a group.' diff --git a/app/graphql/resolvers/tree_resolver.rb b/app/graphql/resolvers/tree_resolver.rb index c07d9187d4d..70b4d81845c 100644 --- a/app/graphql/resolvers/tree_resolver.rb +++ b/app/graphql/resolvers/tree_resolver.rb @@ -6,15 +6,15 @@ module Resolvers calls_gitaly! - argument :path, GraphQL::STRING_TYPE, + argument :path, GraphQL::Types::String, required: false, default_value: '', description: 'The path to get the tree for. Default value is the root of the repository.' - argument :ref, GraphQL::STRING_TYPE, + argument :ref, GraphQL::Types::String, required: false, default_value: :head, description: 'The commit ref to get the tree for. Default value is HEAD.' - argument :recursive, GraphQL::BOOLEAN_TYPE, + argument :recursive, GraphQL::Types::Boolean, required: false, default_value: false, description: 'Used to get a recursive tree. Default is false.' diff --git a/app/graphql/resolvers/user_discussions_count_resolver.rb b/app/graphql/resolvers/user_discussions_count_resolver.rb index 115997ec666..0a04e8df0b8 100644 --- a/app/graphql/resolvers/user_discussions_count_resolver.rb +++ b/app/graphql/resolvers/user_discussions_count_resolver.rb @@ -4,7 +4,7 @@ module Resolvers class UserDiscussionsCountResolver < BaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource - type GraphQL::INT_TYPE, null: true + type GraphQL::Types::Int, null: true def resolve authorize!(object) diff --git a/app/graphql/resolvers/user_merge_requests_resolver_base.rb b/app/graphql/resolvers/user_merge_requests_resolver_base.rb index 221a43f691d..e0201e45664 100644 --- a/app/graphql/resolvers/user_merge_requests_resolver_base.rb +++ b/app/graphql/resolvers/user_merge_requests_resolver_base.rb @@ -5,7 +5,7 @@ module Resolvers include ResolvesProject argument :project_path, - type: GraphQL::STRING_TYPE, + type: GraphQL::Types::String, required: false, description: <<~DESC The full-path of the project the authored merge requests should be in. diff --git a/app/graphql/resolvers/user_notes_count_resolver.rb b/app/graphql/resolvers/user_notes_count_resolver.rb index 2cb61104c18..b91815c72f5 100644 --- a/app/graphql/resolvers/user_notes_count_resolver.rb +++ b/app/graphql/resolvers/user_notes_count_resolver.rb @@ -4,7 +4,7 @@ module Resolvers class UserNotesCountResolver < BaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource - type GraphQL::INT_TYPE, null: true + type GraphQL::Types::Int, null: true def resolve authorize!(object) diff --git a/app/graphql/resolvers/user_resolver.rb b/app/graphql/resolvers/user_resolver.rb index 84bc03091d9..99fd0d4927d 100644 --- a/app/graphql/resolvers/user_resolver.rb +++ b/app/graphql/resolvers/user_resolver.rb @@ -10,7 +10,7 @@ module Resolvers required: false, description: 'ID of the User.' - argument :username, GraphQL::STRING_TYPE, + argument :username, GraphQL::Types::String, required: false, description: 'Username of the User.' diff --git a/app/graphql/resolvers/user_starred_projects_resolver.rb b/app/graphql/resolvers/user_starred_projects_resolver.rb index a8abe759f27..07c6611e41d 100644 --- a/app/graphql/resolvers/user_starred_projects_resolver.rb +++ b/app/graphql/resolvers/user_starred_projects_resolver.rb @@ -4,7 +4,7 @@ module Resolvers class UserStarredProjectsResolver < BaseResolver type Types::ProjectType.connection_type, null: true - argument :search, GraphQL::STRING_TYPE, + argument :search, GraphQL::Types::String, required: false, description: 'Search query.' diff --git a/app/graphql/resolvers/users/group_count_resolver.rb b/app/graphql/resolvers/users/group_count_resolver.rb index ebfe594d31d..592436ac546 100644 --- a/app/graphql/resolvers/users/group_count_resolver.rb +++ b/app/graphql/resolvers/users/group_count_resolver.rb @@ -3,7 +3,7 @@ module Resolvers module Users class GroupCountResolver < BaseResolver - type GraphQL::INT_TYPE, null: true + type GraphQL::Types::Int, null: true alias_method :user, :object diff --git a/app/graphql/resolvers/users_resolver.rb b/app/graphql/resolvers/users_resolver.rb index 95ced131504..c6de3dba41a 100644 --- a/app/graphql/resolvers/users_resolver.rb +++ b/app/graphql/resolvers/users_resolver.rb @@ -7,11 +7,11 @@ module Resolvers type Types::UserType.connection_type, null: true description 'Find Users' - argument :ids, [GraphQL::ID_TYPE], + argument :ids, [GraphQL::Types::ID], required: false, description: 'List of user Global IDs.' - argument :usernames, [GraphQL::STRING_TYPE], required: false, + argument :usernames, [GraphQL::Types::String], required: false, description: 'List of usernames.' argument :sort, Types::SortEnum, @@ -19,11 +19,11 @@ module Resolvers required: false, default_value: :created_desc - argument :search, GraphQL::STRING_TYPE, + argument :search, GraphQL::Types::String, required: false, description: "Query to search users by name, username, or primary email." - argument :admins, GraphQL::BOOLEAN_TYPE, + argument :admins, GraphQL::Types::Boolean, required: false, default_value: false, description: 'Return only admin users.' diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1304bcb1c7e..58f933a7fe0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -413,6 +413,12 @@ module ApplicationHelper end end + def gitlab_ui_form_for(record, *args, &block) + options = args.extract_options! + + form_for(record, *(args << options.merge({ builder: ::Gitlab::FormBuilders::GitlabUiFormBuilder })), &block) + end + private def appearance diff --git a/app/services/packages/debian/generate_distribution_key_service.rb b/app/services/packages/debian/generate_distribution_key_service.rb index 28c97c7681e..917965da58e 100644 --- a/app/services/packages/debian/generate_distribution_key_service.rb +++ b/app/services/packages/debian/generate_distribution_key_service.rb @@ -5,20 +5,42 @@ module Packages class GenerateDistributionKeyService include Gitlab::Utils::StrongMemoize - def initialize(current_user:, params: {}) - @current_user = current_user + def initialize(params: {}) @params = params end def execute - raise ArgumentError, 'Please provide a user' unless current_user.is_a?(User) + using_pinentry do |ctx| + # Generate key + ctx.generate_key generate_key_params + + key = ctx.keys.first # rubocop:disable Gitlab/KeysFirstAndValuesFirst + fingerprint = key.fingerprint + + # Export private key + data = GPGME::Data.new + ctx.export_keys fingerprint, data, GPGME::EXPORT_MODE_SECRET + data.seek 0 + private_key = data.read - generate_key + # Export public key + data = GPGME::Data.new + ctx.export_keys fingerprint, data + data.seek 0 + public_key = data.read + + { + private_key: private_key, + public_key: public_key, + passphrase: passphrase, + fingerprint: fingerprint + } + end end private - attr_reader :current_user, :params + attr_reader :params def passphrase strong_memoize(:passphrase) do @@ -72,35 +94,6 @@ module Packages }.map { |k, v| "#{k}: #{v}\n" }.join + '</GnupgKeyParms>' end - - def generate_key - using_pinentry do |ctx| - # Generate key - ctx.generate_key generate_key_params - - key = ctx.keys.first # rubocop:disable Gitlab/KeysFirstAndValuesFirst - fingerprint = key.fingerprint - - # Export private key - data = GPGME::Data.new - ctx.export_keys fingerprint, data, GPGME::EXPORT_MODE_SECRET - data.seek 0 - private_key = data.read - - # Export public key - data = GPGME::Data.new - ctx.export_keys fingerprint, data - data.seek 0 - public_key = data.read - - { - private_key: private_key, - public_key: public_key, - passphrase: passphrase, - fingerprint: fingerprint - } - end - end end end end diff --git a/app/services/packages/debian/sign_distribution_service.rb b/app/services/packages/debian/sign_distribution_service.rb new file mode 100644 index 00000000000..643995dab67 --- /dev/null +++ b/app/services/packages/debian/sign_distribution_service.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Packages + module Debian + class SignDistributionService + include Gitlab::Utils::StrongMemoize + + def initialize(distribution, content, params: {}) + @distribution = distribution + @content = content + @params = params + end + + def execute + raise ArgumentError, 'distribution key is missing' unless @distribution.key + + sig_mode = GPGME::GPGME_SIG_MODE_CLEAR + + sig_mode = GPGME::GPGME_SIG_MODE_DETACH if @params[:detach] + + Gitlab::Gpg.using_tmp_keychain do + GPGME::Ctx.new( + armor: true, + offline: true, + pinentry_mode: GPGME::PINENTRY_MODE_LOOPBACK, + password: @distribution.key.passphrase + ) do |ctx| + ctx.import(GPGME::Data.from_str(@distribution.key.public_key)) + ctx.import(GPGME::Data.from_str(@distribution.key.private_key)) + signature = GPGME::Data.new + ctx.sign(GPGME::Data.from_str(@content), signature, sig_mode) + signature.to_s + end + end + end + end + end +end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index ec3daf6c494..7ae690b8fe0 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -118,6 +118,7 @@ - if Gitlab::CurrentSettings.version_check_enabled .float-right = version_status_badge + = link_to(sprite_icon('question'), "https://gitlab.com/gitlab-org/gitlab/-/blob/master/CHANGELOG.md", class: 'gl-ml-2', target: '_blank', rel: 'noopener noreferrer') %p = link_to _('GitLab'), general_admin_application_settings_path %span.float-right diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 9b42e1b4967..ae809f01592 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -4,7 +4,6 @@ - page_title @group.name, _("Groups") - current_user_is_group_owner = @group && @group.has_owner?(current_user) -.js-remove-member-modal %h3.page-title = _('Group: %{group_name}') % { group_name: @group.full_name } diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 5c92cbf957e..1a87b21351c 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -5,7 +5,6 @@ - @content_class = "admin-projects" - current_user_is_group_owner = @group && @group.has_owner?(current_user) -.js-remove-member-modal %h3.page-title = _('Project: %{name}') % { name: @project.full_name } = link_to edit_project_path(@project), class: "btn btn-default gl-button float-right" do diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index c5b8c5e25a3..2c0a519fbb8 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -2,7 +2,6 @@ - page_title _('Group members') - groups_select_tag_data = group_select_data(@group).merge({ skip_groups: @skip_groups }) -.js-remove-member-modal .row.gl-mt-3 .col-lg-12 .gl-display-flex.gl-flex-wrap diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index d1f356ed665..9ef1e414872 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -1,4 +1,4 @@ -= form_for @group, html: { multipart: true, class: 'gl-show-field-errors js-general-permissions-form' }, authenticity_token: true do |f| += gitlab_ui_form_for @group, html: { multipart: true, class: 'gl-show-field-errors js-general-permissions-form' }, authenticity_token: true do |f| %input{ type: 'hidden', name: 'update_section', value: 'js-permissions-settings' } = form_errors(@group) @@ -9,12 +9,10 @@ - if @group.root? .form-group.gl-mb-3 - .gl-form-checkbox.custom-control.custom-checkbox - = f.check_box :prevent_sharing_groups_outside_hierarchy, disabled: !can_change_prevent_sharing_groups_outside_hierarchy?(@group), class: 'custom-control-input' - = f.label :prevent_sharing_groups_outside_hierarchy, class: 'custom-control-label' do - %span - = s_('GroupSettings|Prevent members from sending invitations to groups outside of %{group} and its subgroups.').html_safe % { group: link_to_group(@group) } - %p.js-descr.help-text= prevent_sharing_groups_outside_hierarchy_help_text(@group) + = f.gitlab_ui_checkbox_component :prevent_sharing_groups_outside_hierarchy, + s_('GroupSettings|Prevent members from sending invitations to groups outside of %{group} and its subgroups.').html_safe % { group: link_to_group(@group) }, + help_text: prevent_sharing_groups_outside_hierarchy_help_text(@group), + checkbox_options: { disabled: !can_change_prevent_sharing_groups_outside_hierarchy?(@group) } .form-group.gl-mb-3 .gl-form-checkbox.custom-control.custom-checkbox diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 683d3a6ad1b..4fe1039b880 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -32,6 +32,7 @@ - if page_canonical_link %link{ rel: 'canonical', href: page_canonical_link } + = preload_link_tag(path_to_stylesheet('application_utilities')) = yield :prefetch_asset_tags = favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png' @@ -39,12 +40,10 @@ = render 'layouts/startup_css', { startup_filename: local_assigns.fetch(:startup_filename, nil) } - if user_application_theme == 'gl-dark' = stylesheet_link_tag_defer "application_dark" - = yield :page_specific_styles - = stylesheet_link_tag_defer "application_utilities_dark" - else = stylesheet_link_tag_defer "application" - = yield :page_specific_styles - = stylesheet_link_tag_defer "application_utilities" + = yield :page_specific_styles + = stylesheet_link_tag_defer 'application_utilities' = stylesheet_link_tag "disable_animations", media: "all" if Rails.env.test? || Gitlab.config.gitlab['disable_animations'] = stylesheet_link_tag "test_environment", media: "all" if Rails.env.test? diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 187fe608a68..ce02c64623f 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -54,6 +54,8 @@ .settings-content = render 'shared/badges/badge_settings' += render_if_exists 'compliance_management/compliance_framework/project_settings', expanded: expanded + = render_if_exists 'projects/settings/default_issue_template' = render 'projects/service_desk_settings' diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index d1b6db95392..18a5add9704 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,7 +1,6 @@ - add_page_specific_style 'page_bundles/members' - page_title _("Members") -.js-remove-member-modal .row.gl-mt-3 .col-lg-12 - if can_invite_members_for_project?(@project) || can_invite_group_for_project?(@project) diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml index 0891e3e0526..b185f45d129 100644 --- a/app/views/projects/settings/_general.html.haml +++ b/app/views/projects/settings/_general.html.haml @@ -19,8 +19,6 @@ = f.text_field :topics, value: @project.topic_list.join(', '), maxlength: 2000, class: "form-control gl-form-input" %p.form-text.text-muted= _('Separate topics with commas.') - = render_if_exists 'compliance_management/compliance_framework/project_settings', f: f - .row .form-group.col-md-9 = f.label :description, _('Project description (optional)'), class: 'label-bold' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 6aa80e6808d..dc93442d6cd 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -31,8 +31,6 @@ = render 'shared/issuable/form/metadata', issuable: issuable, form: form, project: project, presenter: presenter -= render_if_exists 'shared/issuable/approvals', issuable: issuable, presenter: presenter, form: form - = render 'shared/issuable/form/merge_params', issuable: issuable, project: project = render 'shared/issuable/form/contribution', issuable: issuable, form: form diff --git a/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml b/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml index a0df007f8ca..fad13c78e26 100644 --- a/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml +++ b/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml @@ -8,5 +8,4 @@ = hidden_field_tag "#{issuable.to_ability_name}[reviewer_ids][]", 0, id: nil, data: { meta: '' } = dropdown_tag(users_dropdown_label(issuable.reviewers), options: reviewers_dropdown_options(issuable.to_ability_name, issuable.iid, issuable.target_branch)) - - if Feature.enabled?(:mr_collapsed_approval_rules, @project) - = render_if_exists 'shared/issuable/approver_suggestion', issuable: issuable, presenter: presenter + = render_if_exists 'shared/issuable/approver_suggestion', issuable: issuable, presenter: presenter diff --git a/config/feature_flags/development/mr_collapsed_approval_rules.yml b/config/feature_flags/development/mr_collapsed_approval_rules.yml deleted file mode 100644 index 5fca48ddc3a..00000000000 --- a/config/feature_flags/development/mr_collapsed_approval_rules.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: mr_collapsed_approval_rules -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47475 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/284052 -milestone: '13.6' -type: development -group: group::source code -default_enabled: false diff --git a/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb b/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb index 4bd04edb239..0b409cd2866 100644 --- a/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb +++ b/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb @@ -70,7 +70,7 @@ class MigrateK8sServiceIntegration < ActiveRecord::Migration[5.1] private def parsed_properties - @parsed_properties ||= JSON.parse(self.properties) + @parsed_properties ||= JSON.parse(self.properties) # rubocop:disable Gitlab/Json end end diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md index d63e927627a..7031f15d3a8 100644 --- a/doc/administration/geo/replication/troubleshooting.md +++ b/doc/administration/geo/replication/troubleshooting.md @@ -159,6 +159,27 @@ This machine's Geo node name matches a database record ... no doc/administration/geo/replication/troubleshooting.md#can-geo-detect-the-current-node-correctly ``` +### Message: `WARNING: oldest xmin is far in the past` and `pg_wal` size growing + +If a replication slot is inactive, +the `pg_wal` logs corresponding to the slot are reserved forever +(or until the slot is active again). This causes continuous disk usage growth +and the following messages appear repeatedly in the +[PostgreSQL logs](../../logs.md#postgresql-logs): + +```plaintext +WARNING: oldest xmin is far in the past +HINT: Close open transactions soon to avoid wraparound problems. +You might also need to commit or roll back old prepared transactions, or drop stale replication slots. +``` + +To fix this, do the following: + +1. [Connect to the primary database](https://docs.gitlab.com/omnibus/settings/database.html#connecting-to-the-bundled-postgresql-database). +1. Run `SELECT * FROM pg_replication_slots;`. +1. Note the `slot_name` that reports `active` as `f` (false). +1. Follow [all these steps to remove that Geo site](remove_geo_site.md). + ## Fixing errors found when running the Geo check Rake task When running this Rake task, you may see errors if the nodes are not properly configured: @@ -325,7 +346,8 @@ log data to build up in `pg_xlog`. Removing the unused slots can reduce the amou Slots where `active` is `f` are not active. - When this slot should be active, because you have a **secondary** node configured using that slot, - log in to that **secondary** node and check the PostgreSQL logs why the replication is not running. + log in to that **secondary** node and check the [PostgreSQL logs](../../logs.md#postgresql-logs) + to view why the replication is not running. - If you are no longer using the slot (for example, you no longer have Geo enabled), you can remove it with in the PostgreSQL console session: @@ -521,7 +543,7 @@ to start again from scratch, there are a few steps that can help you: gitlab-ctl stop geo-logcursor ``` - You can watch Sidekiq logs to know when Sidekiq jobs processing have finished: + You can watch the [Sidekiq logs](../../logs.md#sidekiq-logs) to know when Sidekiq jobs processing has finished: ```shell gitlab-ctl tail sidekiq diff --git a/doc/development/fe_guide/haml.md b/doc/development/fe_guide/haml.md new file mode 100644 index 00000000000..eaf475a35b6 --- /dev/null +++ b/doc/development/fe_guide/haml.md @@ -0,0 +1,74 @@ +--- +stage: none +group: unassigned +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# HAML + +[HAML](https://haml.info/) is the [Ruby on Rails](https://rubyonrails.org/) template language that GitLab uses. + +## GitLab UI form builder + +[GitLab UI](https://gitlab-org.gitlab.io/gitlab-ui/) is a Vue component library that conforms +to the [Pajamas design system](https://design.gitlab.com/). Most of these components +rely on JavaScript and therefore can only be used in Vue. + +However, some of the simpler components (checkboxes, radio buttons, form inputs) can be +used in HAML by applying the correct CSS classes to the elements. A custom +[Ruby on Rails form builder](https://gitlab.com/gitlab-org/gitlab/-/blob/7c108df101e86d8a27d69df2b5b1ff1fc24133c5/lib/gitlab/form_builders/gitlab_ui_form_builder.rb) exists to help use GitLab UI components in HAML. + +### Use the GitLab UI form builder + +To use the GitLab UI form builder: + +1. Change `form_for` to `gitlab_ui_form_for`. +1. Change `f.check_box` to `f.gitlab_ui_checkbox_component`. +1. Remove `f.label` and instead pass the label as the second argument in `f.gitlab_ui_checkbox_component`. + +For example: + +- Before: + + ```haml + = gitlab_ui_form_for @group do |f| + .form-group.gl-mb-3 + .gl-form-checkbox.custom-control.custom-checkbox + = f.check_box :prevent_sharing_groups_outside_hierarchy, disabled: !can_change_prevent_sharing_groups_outside_hierarchy?(@group), class: 'custom-control-input' + = f.label :prevent_sharing_groups_outside_hierarchy, class: 'custom-control-label' do + %span + = s_('GroupSettings|Prevent members from sending invitations to groups outside of %{group} and its subgroups.').html_safe % { group: link_to_group(@group) } + %p.help-text= prevent_sharing_groups_outside_hierarchy_help_text(@group) + ``` + +- After: + + ```haml + = gitlab_ui_form_for @group do |f| + .form-group.gl-mb-3 + = f.gitlab_ui_checkbox_component :prevent_sharing_groups_outside_hierarchy, + s_('GroupSettings|Prevent members from sending invitations to groups outside of %{group} and its subgroups.').html_safe % { group: link_to_group(@group) }, + help_text: prevent_sharing_groups_outside_hierarchy_help_text(@group), + checkbox_options: { disabled: !can_change_prevent_sharing_groups_outside_hierarchy?(@group) } + ``` + +### Available components + +When using the GitLab UI form builder, the following components are available for use in HAML. + +NOTE: +Currently only `gitlab_ui_checkbox_component` is available but more components are planned. + +#### gitlab_ui_checkbox_component + +[GitLab UI Docs](https://gitlab-org.gitlab.io/gitlab-ui/?path=/story/base-form-form-checkbox--default) + +| Argument | Description | Type | Required (default value) | +|---|---|---|---| +| `method` | Attribute on the object passed to `gitlab_ui_form_for`. | `Symbol` | `true` | +| `label` | Checkbox label. | `String` | `true` | +| `help_text` | Help text displayed below the checkbox. | `String` | `false` (`nil`) | +| `checkbox_options` | Options that are passed to [Rails `check_box` method](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-check_box). | `Hash` | `false` (`{}`) | +| `checked_value` | Value when checkbox is checked. | `String` | `false` (`'1'`) | +| `unchecked_value` | Value when checkbox is unchecked. | `String` | `false` (`'0'`) | +| `label_options` | Options that are passed to [Rails `label` method](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-label). | `Hash` | `false` (`{}`) | diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index 325310ad05c..517616e155c 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -81,6 +81,10 @@ Vue specific [design patterns and practices](vue.md). How to use [GraphQL](graphql.md). +## HAML + +How to use [HAML](haml.md). + ## Icons and Illustrations How we use SVG for our [Icons and Illustrations](icons.md). diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md index a77c9908ef4..c1733a974e4 100644 --- a/doc/development/sidekiq_style_guide.md +++ b/doc/development/sidekiq_style_guide.md @@ -466,6 +466,24 @@ If we expect an increase of **less than 5%**, then no further action is needed. Otherwise, please ping `@gitlab-org/scalability` on the merge request and ask for a review. +## Job size + +GitLab stores Sidekiq jobs and their arguments in Redis. To avoid +excessive memory usage, we compress the arguments of Sidekiq jobs +if their original size is bigger than 100KB. + +After compression, if their size still exceeds 5MB, it raises an +[`ExceedLimitError`](https://gitlab.com/gitlab-org/gitlab/-/blob/f3dd89e5e510ea04b43ffdcb58587d8f78a8d77c/lib/gitlab/sidekiq_middleware/size_limiter/exceed_limit_error.rb#L8) +error when scheduling the job. + +If this happens, rely on other means of making the data +available in Sidekiq. There are possible workarounds such as: + +- Rebuild the data in Sidekiq with data loaded from the database or + elsewhere. +- Store the data in [object storage](file_storage.md#object-storage) + before scheduling the job, and retrieve it inside the job. + ## Job data consistency strategies In GitLab 13.11 and earlier, Sidekiq workers would always send database queries to the primary diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 73360c3ee4b..b393be18910 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -1050,9 +1050,10 @@ is being discussed in [issue #17517](https://gitlab.com/gitlab-org/gitlab/-/issu ## Alternative backup strategies -If your GitLab server contains a lot of Git repository data, you may find the -GitLab backup script to be too slow. In this case you can consider using -file system snapshots as part of your backup strategy. +If your GitLab instance contains a lot of Git repository data, you may find the +GitLab backup script to be too slow. If your GitLab instance has a lot of forked +projects, the regular backup task also duplicates the Git data for all of them. +In these cases, consider using file system snapshots as part of your backup strategy. Example: Amazon EBS @@ -1074,6 +1075,71 @@ VM snapshots of the entire GitLab server. It's not uncommon however for a VM snapshot to require you to power down the server, which limits this solution's practical use. +### Back up repository data separately + +First, ensure you back up existing GitLab data while [skipping repositories](#excluding-specific-directories-from-the-backup): + +```shell +# for Omnibus GitLab package installations +sudo gitlab-backup create SKIP=repositories + +# for installations from source: +sudo -u git -H bundle exec rake gitlab:backup:create SKIP=repositories RAILS_ENV=production +``` + +For manually backing up the Git repository data on disk, there are multiple possible strategies: + +- Use snapshots, such as the previous examples of Amazon EBS drive snapshots, or LVM snapshots + rsync. +- Use [GitLab Geo](../administration/geo/index.md) and rely on the repository data on a Geo secondary site. +- [Prevent writes and copy the Git repository data](#prevent-writes-and-copy-the-git-repository-data). +- [Create an online backup by marking repositories as read-only (experimental)](#online-backup-through-marking-repositories-as-read-only-experimental). + +#### Prevent writes and copy the Git repository data + +Git repositories must be copied in a consistent way. They should not be copied during concurrent write +operations, as this can lead to inconsistencies or corruption issues. For more details, +[issue 270422](https://gitlab.com/gitlab-org/gitlab/-/issues/270422 "Provide documentation on preferred method of migrating Gitaly servers") +has a longer discussion explaining the potential problems. + +To prevent writes to the Git repository data, there are two possible approaches: + +- Use [maintenance mode](../administration/maintenance_mode/index.md) to place GitLab in a read-only state. +- Create explicit downtime by stopping all Gitaly services before backing up the repositories: + + ```shell + sudo gitlab-ctl stop gitaly + # execute git data copy step + sudo gitlab-ctl start gitaly + ``` + +You can copy Git repository data using any method, as long as writes are prevented on the data being copied +(to prevent inconsistencies and corruption issues). In order of preference and safety, the recommended methods are: + +1. Use `rsync` with archive-mode, delete, and checksum options, for example: + + ```shell + rsync -aR --delete --checksum source destination # be extra safe with the order as it will delete existing data if inverted + ``` + +1. Use a [`tar` pipe to copy the entire repository's directory to another server or location](../administration/operations/moving_repositories.md#tar-pipe-to-another-server). + +1. Use `sftp`, `scp`, `cp`, or any other copying method. + +#### Online backup through marking repositories as read-only (experimental) + +One way of backing up repositories without requiring instance-wide downtime +is to programmatically mark projects as read-only while copying the underlying data. + +There are a few possible downsides to this: + +- Repositories are read-only for a period of time that scales with the size of the repository. +- Backups take a longer time to complete due to marking each project as read-only, potentially leading to inconsistencies. For example, + a possible date discrepancy between the last data available for the first project that gets backed up compared to + the last project that gets backed up. +- Fork networks should be entirely read-only while the projects inside get backed up to prevent potential changes to the pool repository. + +There is an **experimental** script that attempts to automate this process in [Snippet 2149205](https://gitlab.com/-/snippets/2149205). + ## Backup and restore for installations using PgBouncer Do NOT backup or restore GitLab through a PgBouncer connection. These diff --git a/doc/user/infrastructure/iac/index.md b/doc/user/infrastructure/iac/index.md new file mode 100644 index 00000000000..53d8c9864a7 --- /dev/null +++ b/doc/user/infrastructure/iac/index.md @@ -0,0 +1,139 @@ +--- +stage: Configure +group: Configure +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Infrastructure as Code with Terraform and GitLab **(FREE)** + +## Motivation + +The Terraform integration features in GitLab enable your GitOps / Infrastructure-as-Code (IaC) +workflows to tie into GitLab authentication and authorization. These features focus on +lowering the barrier to entry for teams to adopt Terraform, collaborate effectively in +GitLab, and support Terraform best practices. + +## Quick Start + +Use the following `.gitlab-ci.yml` to set up a basic Terraform project integration +for GitLab versions 14.0 and later: + +```yaml +include: + - template: Terraform.gitlab-ci.yml + +variables: + # If not using GitLab's HTTP backend, remove this line and specify TF_HTTP_* variables + TF_STATE_NAME: default + TF_CACHE_KEY: default + # If your terraform files are in a subdirectory, set TF_ROOT accordingly + # TF_ROOT: terraform/production +``` + +This template includes some opinionated decisions, which you can override: + +- Including the latest [GitLab Terraform Image](https://gitlab.com/gitlab-org/terraform-images). +- Using the [GitLab managed Terraform State](#gitlab-managed-terraform-state) as + the Terraform state storage backend. +- Creating [four pipeline stages](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml): + `init`, `validate`, `build`, and `deploy`. These stages + [run the Terraform commands](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml) + `init`, `validate`, `plan`, `plan-json`, and `apply`. The `apply` command only runs on the default branch. + +This video from January 2021 walks you through all the GitLab Terraform integration features: + +<div class="video-fallback"> + See the video: <a href="https://www.youtube.com/watch?v=iGXjUrkkzDI">Terraform with GitLab</a>. +</div> +<figure class="video-container"> + <iframe src="https://www.youtube.com/embed/iGXjUrkkzDI" frameborder="0" allowfullscreen="true"> </iframe> +</figure> + +## GitLab Managed Terraform state + +[Terraform remote backends](https://www.terraform.io/docs/language/settings/backends/index.html) +enable you to store the state file in a remote, shared store. GitLab uses the +[Terraform HTTP backend](https://www.terraform.io/docs/language/settings/backends/http.html) +to securely store the state files in local storage (the default) or +[the remote store of your choice](../../../administration/terraform_state.md). + +The GitLab managed Terraform state backend can store your Terraform state easily and +securely. It spares you from setting up additional remote resources like +Amazon S3 or Google Cloud Storage. Its features include: + +- Supporting encryption of the state file both in transit and at rest. +- Locking and unlocking state. +- Remote Terraform plan and apply execution. + +Read more on setting up and [using GitLab Managed Terraform states](../terraform_state.md) + +WARNING: +Like any other job artifact, Terraform plan data is [viewable by anyone with Guest access](../../permissions.md) to the repository. +Neither Terraform nor GitLab encrypts the plan file by default. If your Terraform plan +includes sensitive data such as passwords, access tokens, or certificates, GitLab strongly +recommends encrypting plan output or modifying the project visibility settings. + +## Terraform module registry + +GitLab can be used as a [Terraform module registry](../../packages/terraform_module_registry/index.md) +to create and publish Terraform modules to a private registry specific to your +top-level namespace. + +## Terraform integration in Merge Requests + +Collaborating around Infrastructure as Code (IaC) changes requires both code changes +and expected infrastructure changes to be checked and approved. GitLab provides a +solution to help collaboration around Terraform code changes and their expected +effects using the Merge Request pages. This way users don't have to build custom +tools or rely on 3rd party solutions to streamline their IaC workflows. + +Read more on setting up and [using the merge request integrations](../mr_integration.md). + +## The GitLab Terraform provider + +WARNING: +The GitLab Terraform provider is released separately from GitLab. +We are working on migrating the GitLab Terraform provider for GitLab.com. + +You can use the [GitLab Terraform provider](https://github.com/gitlabhq/terraform-provider-gitlab) +to manage various aspects of GitLab using Terraform. The provider is an open source project, +owned by GitLab, where everyone can contribute. + +The [documentation of the provider](https://registry.terraform.io/providers/gitlabhq/gitlab/latest/docs) +is available as part of the official Terraform provider documentations. + +## Create a new cluster through IaC + +Learn how to [create a new cluster on Google Kubernetes Engine (GKE)](../clusters/connect/new_gke_cluster.md). + +## Troubleshooting + +### `gitlab_group_share_group` resources not detected when subgroup state is refreshed + +The GitLab Terraform provider can fail to detect existing `gitlab_group_share_group` resources +due to the issue ["User with permissions cannot retrieve `share_with_groups` from the API"](https://gitlab.com/gitlab-org/gitlab/-/issues/328428). +This results in an error when running `terraform apply` because Terraform attempts to recreate an +existing resource. + +For example, consider the following group/subgroup configuration: + +```plaintext +parent-group +├── subgroup-A +└── subgroup-B +``` + +Where: + +- User `user-1` creates `parent-group`, `subgroup-A`, and `subgroup-B`. +- `subgroup-A` is shared with `subgroup-B`. +- User `terraform-user` is member of `parent-group` with inherited `owner` access to both subgroups. + +When the Terraform state is refreshed, the API query `GET /groups/:subgroup-A_id` issued by the provider does not return the +details of `subgroup-B` in the `shared_with_groups` array. This leads to the error. + +To workaround this issue, make sure to apply one of the following conditions: + +1. The `terraform-user` creates all subgroup resources. +1. Grant Maintainer or Owner role to the `terraform-user` user on `subgroup-B`. +1. The `terraform-user` inherited access to `subgroup-B` and `subgroup-B` contains at least one project. diff --git a/doc/user/infrastructure/index.md b/doc/user/infrastructure/index.md index bdaae4b8225..7e334e93c4f 100644 --- a/doc/user/infrastructure/index.md +++ b/doc/user/infrastructure/index.md @@ -4,136 +4,11 @@ group: Configure info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Infrastructure as code with Terraform and GitLab **(FREE)** +# Infrastructure management **(FREE)** -## Motivation +GitLab provides you with great solutions to help you manage your +infrastrucure: -The Terraform integration features in GitLab enable your GitOps / Infrastructure-as-Code (IaC) -workflows to tie into GitLab authentication and authorization. These features focus on -lowering the barrier to entry for teams to adopt Terraform, collaborate effectively in -GitLab, and support Terraform best practices. - -## Quick Start - -Use the following `.gitlab-ci.yml` to set up a basic Terraform project integration -for GitLab versions 14.0 and later: - -```yaml -include: - - template: Terraform.gitlab-ci.yml - -variables: - # If not using GitLab's HTTP backend, remove this line and specify TF_HTTP_* variables - TF_STATE_NAME: default - TF_CACHE_KEY: default - # If your terraform files are in a subdirectory, set TF_ROOT accordingly - # TF_ROOT: terraform/production -``` - -This template includes some opinionated decisions, which you can override: - -- Including the latest [GitLab Terraform Image](https://gitlab.com/gitlab-org/terraform-images). -- Using the [GitLab managed Terraform State](#gitlab-managed-terraform-state) as - the Terraform state storage backend. -- Creating [four pipeline stages](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml): - `init`, `validate`, `build`, and `deploy`. These stages - [run the Terraform commands](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml) - `init`, `validate`, `plan`, `plan-json`, and `apply`. The `apply` command only runs on the default branch. - -This video from January 2021 walks you through all the GitLab Terraform integration features: - -<div class="video-fallback"> - See the video: <a href="https://www.youtube.com/watch?v=iGXjUrkkzDI">Terraform with GitLab</a>. -</div> -<figure class="video-container"> - <iframe src="https://www.youtube.com/embed/iGXjUrkkzDI" frameborder="0" allowfullscreen="true"> </iframe> -</figure> - -## GitLab Managed Terraform state - -[Terraform remote backends](https://www.terraform.io/docs/language/settings/backends/index.html) -enable you to store the state file in a remote, shared store. GitLab uses the -[Terraform HTTP backend](https://www.terraform.io/docs/language/settings/backends/http.html) -to securely store the state files in local storage (the default) or -[the remote store of your choice](../../administration/terraform_state.md). - -The GitLab managed Terraform state backend can store your Terraform state easily and -securely. It spares you from setting up additional remote resources like -Amazon S3 or Google Cloud Storage. Its features include: - -- Supporting encryption of the state file both in transit and at rest. -- Locking and unlocking state. -- Remote Terraform plan and apply execution. - -Read more on setting up and [using GitLab Managed Terraform states](terraform_state.md) - -WARNING: -Like any other job artifact, Terraform plan data is [viewable by anyone with Guest access](../permissions.md) to the repository. -Neither Terraform nor GitLab encrypts the plan file by default. If your Terraform plan -includes sensitive data such as passwords, access tokens, or certificates, GitLab strongly -recommends encrypting plan output or modifying the project visibility settings. - -## Terraform module registry - -GitLab can be used as a [Terraform module registry](../packages/terraform_module_registry/index.md) -to create and publish Terraform modules to a private registry specific to your -top-level namespace. - -## Terraform integration in Merge Requests - -Collaborating around Infrastructure as Code (IaC) changes requires both code changes -and expected infrastructure changes to be checked and approved. GitLab provides a -solution to help collaboration around Terraform code changes and their expected -effects using the Merge Request pages. This way users don't have to build custom -tools or rely on 3rd party solutions to streamline their IaC workflows. - -Read more on setting up and [using the merge request integrations](mr_integration.md). - -## The GitLab Terraform provider - -WARNING: -The GitLab Terraform provider is released separately from GitLab. -We are working on migrating the GitLab Terraform provider for GitLab.com. - -You can use the [GitLab Terraform provider](https://github.com/gitlabhq/terraform-provider-gitlab) -to manage various aspects of GitLab using Terraform. The provider is an open source project, -owned by GitLab, where everyone can contribute. - -The [documentation of the provider](https://registry.terraform.io/providers/gitlabhq/gitlab/latest/docs) -is available as part of the official Terraform provider documentations. - -## Create a new cluster through IaC - -Learn how to [create a new cluster on Google Kubernetes Engine (GKE)](clusters/connect/new_gke_cluster.md). - -## Troubleshooting - -### `gitlab_group_share_group` resources not detected when subgroup state is refreshed - -The GitLab Terraform provider can fail to detect existing `gitlab_group_share_group` resources -due to the issue ["User with permissions cannot retrieve `share_with_groups` from the API"](https://gitlab.com/gitlab-org/gitlab/-/issues/328428). -This results in an error when running `terraform apply` because Terraform attempts to recreate an -existing resource. - -For example, consider the following group/subgroup configuration: - -```plaintext -parent-group -├── subgroup-A -└── subgroup-B -``` - -Where: - -- User `user-1` creates `parent-group`, `subgroup-A`, and `subgroup-B`. -- `subgroup-A` is shared with `subgroup-B`. -- User `terraform-user` is member of `parent-group` with inherited `owner` access to both subgroups. - -When the Terraform state is refreshed, the API query `GET /groups/:subgroup-A_id` issued by the provider does not return the -details of `subgroup-B` in the `shared_with_groups` array. This leads to the error. - -To workaround this issue, make sure to apply one of the following conditions: - -1. The `terraform-user` creates all subgroup resources. -1. Grant Maintainer or Owner role to the `terraform-user` user on `subgroup-B`. -1. The `terraform-user` inherited access to `subgroup-B` and `subgroup-B` contains at least one project. +- [Infrastructure as Code and GitOps](iac/index.md) +- [Kubernetes clusters](../project/clusters/index.md) +- [Runbooks](../project/clusters/runbooks/index.md) diff --git a/doc/user/infrastructure/mr_integration.md b/doc/user/infrastructure/mr_integration.md index 66e00bab6ce..03b3d19cddb 100644 --- a/doc/user/infrastructure/mr_integration.md +++ b/doc/user/infrastructure/mr_integration.md @@ -18,7 +18,7 @@ modifies, or destroys. ## Setup NOTE: -GitLab ships with a [pre-built CI template](index.md#quick-start) that uses GitLab Managed Terraform state and integrates Terraform changes into merge requests. We recommend customizing the pre-built image and relying on the `gitlab-terraform` helper provided within for a quick setup. +GitLab ships with a [pre-built CI template](iac/index.md#quick-start) that uses GitLab Managed Terraform state and integrates Terraform changes into merge requests. We recommend customizing the pre-built image and relying on the `gitlab-terraform` helper provided within for a quick setup. To manually configure a GitLab Terraform Report artifact requires the following steps: diff --git a/lib/gitlab/ci/yaml_processor/dag.rb b/lib/gitlab/ci/yaml_processor/dag.rb index 0140218d9bc..8ab9573dd20 100644 --- a/lib/gitlab/ci/yaml_processor/dag.rb +++ b/lib/gitlab/ci/yaml_processor/dag.rb @@ -23,7 +23,7 @@ module Gitlab new(nodes).tsort rescue TSort::Cyclic - raise ValidationError, 'The pipeline has circular dependencies.' + raise ValidationError, 'The pipeline has circular dependencies' rescue MissingNodeError end diff --git a/lib/gitlab/form_builders/gitlab_ui_form_builder.rb b/lib/gitlab/form_builders/gitlab_ui_form_builder.rb new file mode 100644 index 00000000000..a5290508e42 --- /dev/null +++ b/lib/gitlab/form_builders/gitlab_ui_form_builder.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Gitlab + module FormBuilders + class GitlabUiFormBuilder < ActionView::Helpers::FormBuilder + def gitlab_ui_checkbox_component( + method, + label, + help_text: nil, + checkbox_options: {}, + checked_value: '1', + unchecked_value: '0', + label_options: {} + ) + @template.content_tag( + :div, + class: 'gl-form-checkbox custom-control custom-checkbox' + ) do + @template.check_box( + @object_name, + method, + format_options(checkbox_options, ['custom-control-input']), + checked_value, + unchecked_value + ) + + @template.label( + @object_name, method, format_options(label_options, ['custom-control-label']) + ) do + if help_text + @template.content_tag( + :span, + label + ) + + @template.content_tag( + :p, + help_text, + class: 'help-text' + ) + else + label + end + end + end + end + + private + + def format_options(options, classes) + classes << options[:class] + + objectify_options(options.merge({ class: classes.flatten.compact })) + end + end + end +end diff --git a/lib/gitlab/import_export/json/legacy_reader.rb b/lib/gitlab/import_export/json/legacy_reader.rb index 97b34088e3e..dc80c92f507 100644 --- a/lib/gitlab/import_export/json/legacy_reader.rb +++ b/lib/gitlab/import_export/json/legacy_reader.rb @@ -27,7 +27,7 @@ module Gitlab end def read_hash - ActiveSupport::JSON.decode(IO.read(@path)) + Gitlab::Json.parse(IO.read(@path)) rescue StandardError => e Gitlab::ErrorTracking.log_exception(e) raise Gitlab::ImportExport::Error, 'Incorrect JSON format' diff --git a/lib/gitlab/import_export/json/ndjson_reader.rb b/lib/gitlab/import_export/json/ndjson_reader.rb index 4899bd3b0ee..510da61d3ab 100644 --- a/lib/gitlab/import_export/json/ndjson_reader.rb +++ b/lib/gitlab/import_export/json/ndjson_reader.rb @@ -47,8 +47,8 @@ module Gitlab private def json_decode(string) - ActiveSupport::JSON.decode(string) - rescue ActiveSupport::JSON.parse_error => e + Gitlab::Json.parse(string) + rescue JSON::ParserError => e Gitlab::ErrorTracking.log_exception(e) raise Gitlab::ImportExport::Error, 'Incorrect JSON format' end diff --git a/lib/gitlab/import_export/lfs_restorer.rb b/lib/gitlab/import_export/lfs_restorer.rb index d73ae1410a3..9931b09e9ca 100644 --- a/lib/gitlab/import_export/lfs_restorer.rb +++ b/lib/gitlab/import_export/lfs_restorer.rb @@ -72,7 +72,7 @@ module Gitlab @lfs_json ||= begin json = IO.read(lfs_json_path) - ActiveSupport::JSON.decode(json) + Gitlab::Json.parse(json) rescue StandardError raise Gitlab::ImportExport::Error, 'Incorrect JSON format' end diff --git a/lib/gitlab/json_cache.rb b/lib/gitlab/json_cache.rb index 4314c131ada..41c18f82a4b 100644 --- a/lib/gitlab/json_cache.rb +++ b/lib/gitlab/json_cache.rb @@ -58,7 +58,7 @@ module Gitlab private def parse_value(raw, klass) - value = ActiveSupport::JSON.decode(raw.to_s) + value = Gitlab::Json.parse(raw.to_s) case value when Hash then parse_entry(value, klass) @@ -66,7 +66,7 @@ module Gitlab else value end - rescue ActiveSupport::JSON.parse_error + rescue JSON::ParserError nil end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e70f1465b97..a7275a62559 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16496,9 +16496,6 @@ msgstr "" msgid "Image details" msgstr "" -msgid "ImageAltText|Sparkling golden tanuki logo" -msgstr "" - msgid "ImageDiffViewer|2-up" msgstr "" @@ -28270,6 +28267,9 @@ msgstr "" msgid "Runners|Something went wrong while fetching the tags suggestions" msgstr "" +msgid "Runners|Stop the runner from accepting new jobs." +msgstr "" + msgid "Runners|Tags" msgstr "" @@ -28291,6 +28291,15 @@ msgstr "" msgid "Runners|Use Group runners when you want all projects in a group to have access to a set of runners." msgstr "" +msgid "Runners|Use the runner for jobs without tags, in addition to tagged jobs." +msgstr "" + +msgid "Runners|Use the runner for the currently assigned projects only." +msgstr "" + +msgid "Runners|Use the runner on pipelines for protected branches only." +msgstr "" + msgid "Runners|Value" msgstr "" @@ -29342,6 +29351,9 @@ msgstr "" msgid "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes." msgstr "" +msgid "Select a framework that applies to this project. %{linkStart}How are these added?%{linkEnd}" +msgstr "" + msgid "Select a group to invite" msgstr "" @@ -34126,9 +34138,6 @@ msgstr "" msgid "Tip: Hover over a job to see the jobs it depends on to run." msgstr "" -msgid "Tip: add a" -msgstr "" - msgid "Tip: add a %{linkStart}CODEOWNERS%{linkEnd} to automatically add approvers based on file paths and file types." msgstr "" @@ -38955,9 +38964,6 @@ msgid_plural "merge requests" msgstr[0] "" msgstr[1] "" -msgid "merge request approvals" -msgstr "" - msgid "merged %{timeAgo}" msgstr "" @@ -39655,9 +39661,6 @@ msgstr "" msgid "time summary" msgstr "" -msgid "to automatically add approvers based on file paths and file types." -msgstr "" - msgid "to help your contributors communicate effectively!" msgstr "" diff --git a/qa/qa/page/group/members.rb b/qa/qa/page/group/members.rb index b526a4488b2..ccc901932f4 100644 --- a/qa/qa/page/group/members.rb +++ b/qa/qa/page/group/members.rb @@ -7,7 +7,7 @@ module QA include Page::Component::InviteMembersModal include Page::Component::UsersSelect - view 'app/assets/javascripts/vue_shared/components/remove_member_modal.vue' do + view 'app/assets/javascripts/members/components/modals/remove_member_modal.vue' do element :remove_member_modal_content end diff --git a/rubocop/cop/gitlab/json.rb b/rubocop/cop/gitlab/json.rb index 7cc719aca09..d2ba0012ca0 100644 --- a/rubocop/cop/gitlab/json.rb +++ b/rubocop/cop/gitlab/json.rb @@ -10,7 +10,7 @@ module RuboCop EOL def_node_matcher :json_node?, <<~PATTERN - (send (const nil? :JSON)...) + (send (const {nil? | (const nil? :ActiveSupport)} :JSON)...) PATTERN def on_send(node) diff --git a/spec/factories/packages/debian/distribution_key.rb b/spec/factories/packages/debian/distribution_key.rb index 6bd457c50d0..94e041eb4a9 100644 --- a/spec/factories/packages/debian/distribution_key.rb +++ b/spec/factories/packages/debian/distribution_key.rb @@ -4,9 +4,9 @@ FactoryBot.define do factory :debian_project_distribution_key, class: 'Packages::Debian::ProjectDistributionKey' do distribution { association(:debian_project_distribution) } - private_key { '-----BEGIN PGP PRIVATE KEY BLOCK-----' } + private_key { File.read(Rails.root.join('spec/fixtures/', 'private_key.asc')) } passphrase { '12345' } - public_key { '-----BEGIN PGP PUBLIC KEY BLOCK-----' } + public_key { File.read(Rails.root.join('spec/fixtures/', 'public_key.asc')) } fingerprint { '12345' } factory :debian_group_distribution_key, class: 'Packages::Debian::GroupDistributionKey' do diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb index 7f8ded4fa43..ccf3ccc6a96 100644 --- a/spec/features/projects/import_export/export_file_spec.rb +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -82,8 +82,7 @@ RSpec.describe 'Import/Export - project export integration test', :js do relations << Gitlab::Json.parse(IO.read(project_json_path)) Dir.glob(File.join(tmpdir, 'tree/project', '*.ndjson')) do |rb_filename| File.foreach(rb_filename) do |line| - json = ActiveSupport::JSON.decode(line) - relations << json + relations << Gitlab::Json.parse(line) end end diff --git a/spec/fixtures/private_key.asc b/spec/fixtures/private_key.asc new file mode 100644 index 00000000000..d9252bafd62 --- /dev/null +++ b/spec/fixtures/private_key.asc @@ -0,0 +1,17 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: Alice's OpenPGP Transferable Secret Key +Comment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html + +lFgEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U +b7O1u10AAP9XBeW6lzGOLx7zHH9AsUDUTb2pggYGMzd0P3ulJ2AfvQ4RtCZBbGlj +ZSBMb3ZlbGFjZSA8YWxpY2VAb3BlbnBncC5leGFtcGxlPoiQBBMWCAA4AhsDBQsJ +CAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE64W7X6M6deFelE5j8jFVDE9H444FAl2l +nzoACgkQ8jFVDE9H447pKwD6A5xwUqIDprBzrHfahrImaYEZzncqb25vkLV2arYf +a78A/R3AwtLQvjxwLDuzk4dUtUwvUYibL2sAHwj2kGaHnfICnF0EXEcE6RIKKwYB +BAGXVQEFAQEHQEL/BiGtq0k84Km1wqQw2DIikVYrQrMttN8d7BPfnr4iAwEIBwAA +/3/xFPG6U17rhTuq+07gmEvaFYKfxRB6sgAYiW6TMTpQEK6IeAQYFggAIBYhBOuF +u1+jOnXhXpROY/IxVQxPR+OOBQJcRwTpAhsMAAoJEPIxVQxPR+OOWdABAMUdSzpM +hzGs1O0RkWNQWbUzQ8nUOeD9wNbjE3zR+yfRAQDbYqvtWQKN4AQLTxVJN5X5AWyb +Pnn+We1aTBhaGa86AQ== +=n8OM +-----END PGP PRIVATE KEY BLOCK----- diff --git a/spec/fixtures/public_key.asc b/spec/fixtures/public_key.asc new file mode 100644 index 00000000000..68fdb39324e --- /dev/null +++ b/spec/fixtures/public_key.asc @@ -0,0 +1,15 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: Alice's OpenPGP certificate +Comment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html + +mDMEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U +b7O1u120JkFsaWNlIExvdmVsYWNlIDxhbGljZUBvcGVucGdwLmV4YW1wbGU+iJAE +ExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTrhbtfozp14V6UTmPy +MVUMT0fjjgUCXaWfOgAKCRDyMVUMT0fjjukrAPoDnHBSogOmsHOsd9qGsiZpgRnO +dypvbm+QtXZqth9rvwD9HcDC0tC+PHAsO7OTh1S1TC9RiJsvawAfCPaQZoed8gK4 +OARcRwTpEgorBgEEAZdVAQUBAQdAQv8GIa2rSTzgqbXCpDDYMiKRVitCsy203x3s +E9+eviIDAQgHiHgEGBYIACAWIQTrhbtfozp14V6UTmPyMVUMT0fjjgUCXEcE6QIb +DAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn +0QEA22Kr7VkCjeAEC08VSTeV+QFsmz55/lntWkwYWhmvOgE= +=iIGO +-----END PGP PUBLIC KEY BLOCK----- diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js index 43b9c5871a6..2dd35519464 100644 --- a/spec/frontend/diffs/components/settings_dropdown_spec.js +++ b/spec/frontend/diffs/components/settings_dropdown_spec.js @@ -48,13 +48,17 @@ describe('Diff settings dropdown component', () => { it('list view button dispatches setRenderTreeList with false', () => { wrapper.find('.js-list-view').trigger('click'); - expect(store.dispatch).toHaveBeenCalledWith('diffs/setRenderTreeList', false); + expect(store.dispatch).toHaveBeenCalledWith('diffs/setRenderTreeList', { + renderTreeList: false, + }); }); it('tree view button dispatches setRenderTreeList with true', () => { wrapper.find('.js-tree-view').trigger('click'); - expect(store.dispatch).toHaveBeenCalledWith('diffs/setRenderTreeList', true); + expect(store.dispatch).toHaveBeenCalledWith('diffs/setRenderTreeList', { + renderTreeList: true, + }); }); it('sets list button as selected when renderTreeList is false', () => { diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index c2e5d07bcfd..4681b4f9d45 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -1000,7 +1000,7 @@ describe('DiffsStoreActions', () => { it('commits SET_RENDER_TREE_LIST', (done) => { testAction( setRenderTreeList, - true, + { renderTreeList: true }, {}, [{ type: types.SET_RENDER_TREE_LIST, payload: true }], [], @@ -1009,7 +1009,7 @@ describe('DiffsStoreActions', () => { }); it('sets localStorage', () => { - setRenderTreeList({ commit() {} }, true); + setRenderTreeList({ commit() {} }, { renderTreeList: true }); expect(localStorage.setItem).toHaveBeenCalledWith('mr_diff_tree_list', true); }); diff --git a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js index e7a99a96da6..79252456f67 100644 --- a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js +++ b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js @@ -37,7 +37,7 @@ describe('InviteActionButtons', () => { }); it('sets props correctly', () => { - expect(findRemoveMemberButton().props()).toEqual({ + expect(findRemoveMemberButton().props()).toMatchObject({ memberId: member.id, memberType: null, message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.fullName}"`, diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js index 4ff12f7fa97..d8453d453e7 100644 --- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js +++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js @@ -1,6 +1,8 @@ +import { GlButton } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { modalData } from 'jest/members/mock_data'; import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue'; import { MEMBER_TYPES } from '~/members/constants'; @@ -10,6 +12,10 @@ localVue.use(Vuex); describe('RemoveMemberButton', () => { let wrapper; + const actions = { + showRemoveMemberModal: jest.fn(), + }; + const createStore = (state = {}) => { return new Vuex.Store({ modules: { @@ -19,6 +25,7 @@ describe('RemoveMemberButton', () => { memberPath: '/groups/foo-bar/-/group_members/:id', ...state, }, + actions, }, }, }); @@ -47,20 +54,16 @@ describe('RemoveMemberButton', () => { }); }; + beforeEach(() => { + createComponent(); + }); + afterEach(() => { wrapper.destroy(); }); it('sets attributes on button', () => { - createComponent(); - expect(wrapper.attributes()).toMatchObject({ - 'data-member-path': '/groups/foo-bar/-/group_members/1', - 'data-member-type': 'GroupMember', - 'data-message': 'Are you sure you want to remove John Smith?', - 'data-is-access-request': 'true', - 'data-is-invite': 'true', - 'data-oncall-schedules': '{"name":"user","schedules":[]}', 'aria-label': 'Remove member', title: 'Remove member', icon: 'remove', @@ -68,14 +71,12 @@ describe('RemoveMemberButton', () => { }); it('displays `title` prop as a tooltip', () => { - createComponent(); - expect(getBinding(wrapper.element, 'gl-tooltip')).not.toBeUndefined(); }); - it('has CSS class used by `remove_member_modal.vue`', () => { - createComponent(); + it('calls Vuex action to show `remove member` modal when clicked', () => { + wrapper.findComponent(GlButton).vm.$emit('click'); - expect(wrapper.classes()).toContain('js-remove-member-button'); + expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), modalData); }); }); diff --git a/spec/frontend/vue_shared/components/remove_member_modal_spec.js b/spec/frontend/members/components/modals/remove_member_modal_spec.js index ce9de28d53c..1dc41582c12 100644 --- a/spec/frontend/vue_shared/components/remove_member_modal_spec.js +++ b/spec/frontend/members/components/modals/remove_member_modal_spec.js @@ -1,37 +1,61 @@ import { GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import RemoveMemberModal from '~/members/components/modals/remove_member_modal.vue'; +import { MEMBER_TYPES } from '~/members/constants'; import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue'; -import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; -const mockSchedules = JSON.stringify({ - schedules: [ - { - id: 1, - name: 'Schedule 1', - }, - ], - name: 'User1', -}); +Vue.use(Vuex); describe('RemoveMemberModal', () => { const memberPath = '/gitlab-org/gitlab-test/-/project_members/90'; + const mockSchedules = { + name: 'User1', + schedules: [{ id: 1, name: 'Schedule 1' }], + }; let wrapper; + const actions = { + hideRemoveMemberModal: jest.fn(), + }; + + const createStore = (removeMemberModalData) => + new Vuex.Store({ + modules: { + [MEMBER_TYPES.user]: { + namespaced: true, + state: { + removeMemberModalData, + }, + actions, + }, + }, + }); + + const createComponent = (state) => { + wrapper = shallowMount(RemoveMemberModal, { + store: createStore(state), + provide: { + namespace: MEMBER_TYPES.user, + }, + }); + }; + const findForm = () => wrapper.find({ ref: 'form' }); const findGlModal = () => wrapper.findComponent(GlModal); const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList); afterEach(() => { wrapper.destroy(); - wrapper = null; }); describe.each` - state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | onCallSchedules - ${'removing a group member'} | ${'GroupMember'} | ${false} | ${'false'} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${`{}`} - ${'removing a project member'} | ${'ProjectMember'} | ${false} | ${'false'} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockSchedules} - ${'denying an access request'} | ${'ProjectMember'} | ${true} | ${'false'} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${`{}`} - ${'revoking invite'} | ${'ProjectMember'} | ${false} | ${'true'} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockSchedules} + state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | onCallSchedules + ${'removing a group member'} | ${'GroupMember'} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}} + ${'removing a project member'} | ${'ProjectMember'} | ${false} | ${false} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockSchedules} + ${'denying an access request'} | ${'ProjectMember'} | ${true} | ${false} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${{}} + ${'revoking invite'} | ${'ProjectMember'} | ${false} | ${true} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockSchedules} `( 'when $state', ({ @@ -45,24 +69,17 @@ describe('RemoveMemberModal', () => { onCallSchedules, }) => { beforeEach(() => { - wrapper = shallowMount(RemoveMemberModal, { - data() { - return { - modalData: { - isAccessRequest, - isInvite, - message, - memberPath, - memberType, - onCallSchedules, - }, - }; - }, + createComponent({ + isAccessRequest, + isInvite, + message, + memberPath, + memberType, + onCallSchedules, }); }); - const parsedSchedules = JSON.parse(onCallSchedules); - const isPartOfOncallSchedules = Boolean(isAccessRequest && parsedSchedules.schedules?.length); + const isPartOfOncallSchedules = Boolean(isAccessRequest && onCallSchedules.schedules?.length); it(`has the title ${actionText}`, () => { expect(findGlModal().attributes('title')).toBe(actionText); @@ -73,7 +90,7 @@ describe('RemoveMemberModal', () => { }); it('displays a message to the user', () => { - expect(wrapper.find('[data-testid=modal-message]').text()).toBe(message); + expect(wrapper.find('p').text()).toBe(message); }); it(`shows ${ @@ -105,6 +122,12 @@ describe('RemoveMemberModal', () => { spy.mockRestore(); }); + + it('calls Vuex action to hide the modal when `GlModal` emits `hide` event', () => { + findGlModal().vm.$emit('hide'); + + expect(actions.hideRemoveMemberModal).toHaveBeenCalled(); + }); }, ); }); diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js index 3a17d78bd17..6e7d9bc6b6e 100644 --- a/spec/frontend/members/components/table/members_table_spec.js +++ b/spec/frontend/members/components/table/members_table_spec.js @@ -72,6 +72,7 @@ describe('MembersTable', () => { 'member-action-buttons', 'role-dropdown', 'remove-group-link-modal', + 'remove-member-modal', 'expiration-datepicker', ], }); diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js index 4275db5fa9f..eb9f905fea2 100644 --- a/spec/frontend/members/mock_data.js +++ b/spec/frontend/members/mock_data.js @@ -57,6 +57,15 @@ export const group = { validRoles: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 }, }; +export const modalData = { + isAccessRequest: true, + isInvite: true, + memberPath: '/groups/foo-bar/-/group_members/1', + memberType: 'GroupMember', + message: 'Are you sure you want to remove John Smith?', + oncallSchedules: { name: 'user', schedules: [] }, +}; + const { user, ...memberNoUser } = member; export const invite = { ...memberNoUser, diff --git a/spec/frontend/members/store/actions_spec.js b/spec/frontend/members/store/actions_spec.js index d913c5c56df..d37e6871387 100644 --- a/spec/frontend/members/store/actions_spec.js +++ b/spec/frontend/members/store/actions_spec.js @@ -3,12 +3,14 @@ import MockAdapter from 'axios-mock-adapter'; import { noop } from 'lodash'; import { useFakeDate } from 'helpers/fake_date'; import testAction from 'helpers/vuex_action_helper'; -import { members, group } from 'jest/members/mock_data'; +import { members, group, modalData } from 'jest/members/mock_data'; import httpStatusCodes from '~/lib/utils/http_status'; import { updateMemberRole, showRemoveGroupLinkModal, hideRemoveGroupLinkModal, + showRemoveMemberModal, + hideRemoveMemberModal, updateMemberExpiration, } from '~/members/store/actions'; import * as types from '~/members/store/mutation_types'; @@ -153,4 +155,32 @@ describe('Vuex members actions', () => { }); }); }); + + describe('Remove member modal', () => { + const state = { + removeMemberModalVisible: false, + removeMemberModalData: {}, + }; + + describe('showRemoveMemberModal', () => { + it(`commits ${types.SHOW_REMOVE_MEMBER_MODAL} mutation`, () => { + testAction(showRemoveMemberModal, modalData, state, [ + { + type: types.SHOW_REMOVE_MEMBER_MODAL, + payload: modalData, + }, + ]); + }); + }); + + describe('hideRemoveMemberModal', () => { + it(`commits ${types.HIDE_REMOVE_MEMBER_MODAL} mutation`, () => { + testAction(hideRemoveMemberModal, undefined, state, [ + { + type: types.HIDE_REMOVE_MEMBER_MODAL, + }, + ]); + }); + }); + }); }); diff --git a/spec/frontend/members/store/mutations_spec.js b/spec/frontend/members/store/mutations_spec.js index 7ad7034eb6d..72423c6b4d4 100644 --- a/spec/frontend/members/store/mutations_spec.js +++ b/spec/frontend/members/store/mutations_spec.js @@ -1,4 +1,4 @@ -import { members, group } from 'jest/members/mock_data'; +import { members, group, modalData } from 'jest/members/mock_data'; import * as types from '~/members/store/mutation_types'; import mutations from '~/members/store/mutations'; @@ -154,4 +154,32 @@ describe('Vuex members mutations', () => { expect(state.removeGroupLinkModalVisible).toBe(false); }); }); + + describe(types.SHOW_REMOVE_MEMBER_MODAL, () => { + it('sets `removeMemberModalVisible` and `removeMemberModalData`', () => { + const state = { + removeMemberModalVisible: false, + removeMemberModalData: {}, + }; + + mutations[types.SHOW_REMOVE_MEMBER_MODAL](state, modalData); + + expect(state).toEqual({ + removeMemberModalVisible: true, + removeMemberModalData: modalData, + }); + }); + }); + + describe(types.HIDE_REMOVE_MEMBER_MODAL, () => { + it('sets `removeMemberModalVisible` to `false`', () => { + const state = { + removeMemberModalVisible: true, + }; + + mutations[types.HIDE_REMOVE_MEMBER_MODAL](state); + + expect(state.removeMemberModalVisible).toBe(false); + }); + }); }); diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js index 3658dbb5ef2..fdb1d2f86e3 100644 --- a/spec/frontend/security_configuration/components/feature_card_spec.js +++ b/spec/frontend/security_configuration/components/feature_card_spec.js @@ -127,25 +127,35 @@ describe('FeatureCard component', () => { describe('actions', () => { describe.each` - context | type | available | configured | configurationPath | canEnableByMergeRequest | action - ${'unavailable'} | ${REPORT_TYPE_SAST} | ${false} | ${false} | ${null} | ${false} | ${null} - ${'available'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${null} | ${false} | ${'guide'} - ${'configured'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${null} | ${false} | ${'guide'} - ${'available, can enable by MR'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${null} | ${true} | ${'create-mr'} - ${'available, can enable by MR, unknown type'} | ${'foo'} | ${true} | ${false} | ${null} | ${true} | ${'guide'} - ${'configured, can enable by MR'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${null} | ${true} | ${'guide'} - ${'available with config path'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${'foo'} | ${false} | ${'enable'} - ${'available with config path, can enable by MR'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${'foo'} | ${true} | ${'enable'} - ${'configured with config path'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${'foo'} | ${false} | ${'configure'} - ${'configured with config path, can enable by MR'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${'foo'} | ${true} | ${'configure'} + context | type | available | configured | configurationHelpPath | configurationPath | canEnableByMergeRequest | action + ${'unavailable'} | ${REPORT_TYPE_SAST} | ${false} | ${false} | ${'/help'} | ${null} | ${false} | ${null} + ${'available, no configurationHelpPath'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${null} | ${null} | ${false} | ${null} + ${'available'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${'/help'} | ${null} | ${false} | ${'guide'} + ${'configured'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${'/help'} | ${null} | ${false} | ${'guide'} + ${'available, can enable by MR'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${'/help'} | ${null} | ${true} | ${'create-mr'} + ${'available, can enable by MR, unknown type'} | ${'foo'} | ${true} | ${false} | ${'/help'} | ${null} | ${true} | ${'guide'} + ${'configured, can enable by MR'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${'/help'} | ${null} | ${true} | ${'guide'} + ${'available with config path'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${'/help'} | ${'foo'} | ${false} | ${'enable'} + ${'available with config path, can enable by MR'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${'/help'} | ${'foo'} | ${true} | ${'enable'} + ${'configured with config path'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${'/help'} | ${'foo'} | ${false} | ${'configure'} + ${'configured with config path, can enable by MR'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${'/help'} | ${'foo'} | ${true} | ${'configure'} `( 'given $context feature', - ({ type, available, configured, configurationPath, canEnableByMergeRequest, action }) => { + ({ + type, + available, + configured, + configurationHelpPath, + configurationPath, + canEnableByMergeRequest, + action, + }) => { beforeEach(() => { feature = makeFeature({ type, available, configured, + configurationHelpPath, configurationPath, canEnableByMergeRequest, }); diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb index 8d2ae238bfe..d77a0b6242e 100644 --- a/spec/graphql/resolvers/base_resolver_spec.rb +++ b/spec/graphql/resolvers/base_resolver_spec.rb @@ -7,8 +7,8 @@ RSpec.describe Resolvers::BaseResolver do let(:resolver) do Class.new(described_class) do - argument :test, ::GraphQL::INT_TYPE, required: false - type [::GraphQL::INT_TYPE], null: true + argument :test, ::GraphQL::Types::Int, required: false + type [::GraphQL::Types::Int], null: true def resolve(test: 100) process(object) @@ -22,7 +22,7 @@ RSpec.describe Resolvers::BaseResolver do let(:last_resolver) do Class.new(described_class) do - type [::GraphQL::INT_TYPE], null: true + type [::GraphQL::Types::Int], null: true def resolve(**args) [1, 2] @@ -36,11 +36,11 @@ RSpec.describe Resolvers::BaseResolver do context 'for a connection of scalars' do let(:resolver) do Class.new(described_class) do - type ::GraphQL::INT_TYPE.connection_type, null: true + type ::GraphQL::Types::Int.connection_type, null: true end end - it { is_expected.to eq(::GraphQL::INT_TYPE) } + it { is_expected.to eq(::GraphQL::Types::Int) } end context 'for a connection of objects' do @@ -64,21 +64,21 @@ RSpec.describe Resolvers::BaseResolver do context 'for a list type' do let(:resolver) do Class.new(described_class) do - type [::GraphQL::STRING_TYPE], null: true + type [::GraphQL::Types::String], null: true end end - it { is_expected.to eq(::GraphQL::STRING_TYPE) } + it { is_expected.to eq(::GraphQL::Types::String) } end context 'for a scalar type' do let(:resolver) do Class.new(described_class) do - type ::GraphQL::BOOLEAN_TYPE, null: true + type ::GraphQL::Types::Boolean, null: true end end - it { is_expected.to eq(::GraphQL::BOOLEAN_TYPE) } + it { is_expected.to eq(::GraphQL::Types::Boolean) } end end @@ -88,7 +88,7 @@ RSpec.describe Resolvers::BaseResolver do end it 'has the correct (singular) type' do - expect(resolver.single.type).to eq(::GraphQL::INT_TYPE) + expect(resolver.single.type).to eq(::GraphQL::Types::Int) end it 'returns the same subclass every time' do @@ -105,10 +105,10 @@ RSpec.describe Resolvers::BaseResolver do describe '.when_single' do let(:resolver) do Class.new(described_class) do - type [::GraphQL::INT_TYPE], null: true + type [::GraphQL::Types::Int], null: true when_single do - argument :foo, ::GraphQL::INT_TYPE, required: true + argument :foo, ::GraphQL::Types::Int, required: true end def resolve(foo: 1) @@ -138,14 +138,14 @@ RSpec.describe Resolvers::BaseResolver do context 'multiple when_single blocks' do let(:resolver) do Class.new(described_class) do - type [::GraphQL::INT_TYPE], null: true + type [::GraphQL::Types::Int], null: true when_single do - argument :foo, ::GraphQL::INT_TYPE, required: true + argument :foo, ::GraphQL::Types::Int, required: true end when_single do - argument :bar, ::GraphQL::INT_TYPE, required: true + argument :bar, ::GraphQL::Types::Int, required: true end def resolve(foo: 1, bar: 2) @@ -168,7 +168,7 @@ RSpec.describe Resolvers::BaseResolver do let(:subclass) do Class.new(resolver) do when_single do - argument :inc, ::GraphQL::INT_TYPE, required: true + argument :inc, ::GraphQL::Types::Int, required: true end def resolve(foo:, inc:) @@ -194,7 +194,7 @@ RSpec.describe Resolvers::BaseResolver do context 'when the resolver returns early' do let(:resolver) do Class.new(described_class) do - type [::GraphQL::STRING_TYPE], null: true + type [::GraphQL::Types::String], null: true def ready?(**args) [false, %w[early return]] @@ -237,14 +237,14 @@ RSpec.describe Resolvers::BaseResolver do context 'when field is a connection' do it 'increases complexity based on arguments' do - field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 1) + field = Types::BaseField.new(name: 'test', type: GraphQL::Types::String.connection_type, resolver_class: described_class, null: false, max_page_size: 1) expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 3 expect(field.to_graphql.complexity.call({}, { search: 'foo' }, 1)).to eq 7 end it 'does not increase complexity when filtering by iids' do - field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 100) + field = Types::BaseField.new(name: 'test', type: GraphQL::Types::String.connection_type, resolver_class: described_class, null: false, max_page_size: 100) expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 6 expect(field.to_graphql.complexity.call({}, { sort: 'foo', iid: 1 }, 1)).to eq 3 diff --git a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb index 8d15d7eda1b..852aaf66201 100644 --- a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb +++ b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb @@ -20,7 +20,7 @@ RSpec.describe ::CachingArrayResolver do Class.new(::Resolvers::BaseResolver) do include mod type [::Types::UserType], null: true - argument :is_admin, ::GraphQL::BOOLEAN_TYPE, required: false + argument :is_admin, ::GraphQL::Types::Boolean, required: false def query_input(is_admin:) is_admin @@ -50,7 +50,7 @@ RSpec.describe ::CachingArrayResolver do Class.new(::Resolvers::BaseResolver) do include mod type [::Types::UserType], null: true - argument :username, ::GraphQL::STRING_TYPE, required: false + argument :username, ::GraphQL::Types::String, required: false def query_input(username:) username diff --git a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb index 3dffda75e08..6f6855c8f84 100644 --- a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb +++ b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb @@ -50,7 +50,7 @@ RSpec.describe ResolvesPipelines do end it 'increases field complexity based on arguments' do - field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: false, max_page_size: 1) + field = Types::BaseField.new(name: 'test', type: GraphQL::Types::String, resolver_class: resolver, null: false, max_page_size: 1) expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 2 expect(field.to_graphql.complexity.call({}, { sha: 'foo' }, 1)).to eq 4 diff --git a/spec/graphql/resolvers/echo_resolver_spec.rb b/spec/graphql/resolvers/echo_resolver_spec.rb index 4f48e5e0d7a..59a121ac7de 100644 --- a/spec/graphql/resolvers/echo_resolver_spec.rb +++ b/spec/graphql/resolvers/echo_resolver_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Resolvers::EchoResolver do let(:text) { 'Message test' } specify do - expect(described_class).to have_non_null_graphql_type(::GraphQL::STRING_TYPE) + expect(described_class).to have_non_null_graphql_type(::GraphQL::Types::String) end describe '#resolve' do diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 9b329e961cc..89e4e803fa8 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -407,7 +407,7 @@ RSpec.describe Resolvers::IssuesResolver do end it 'increases field complexity based on arguments' do - field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 100) + field = Types::BaseField.new(name: 'test', type: GraphQL::Types::String.connection_type, resolver_class: described_class, null: false, max_page_size: 100) expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 4 expect(field.to_graphql.complexity.call({}, { labelName: 'foo' }, 1)).to eq 8 diff --git a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb index 618d012bd6d..b1f50a4a4a5 100644 --- a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb +++ b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb @@ -145,7 +145,7 @@ RSpec.describe Resolvers::NamespaceProjectsResolver do end it 'has an high complexity regardless of arguments' do - field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 100) + field = Types::BaseField.new(name: 'test', type: GraphQL::Types::String.connection_type, resolver_class: described_class, null: false, max_page_size: 100) expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 24 expect(field.to_graphql.complexity.call({}, { include_subgroups: true }, 1)).to eq 24 diff --git a/spec/graphql/resolvers/project_resolver_spec.rb b/spec/graphql/resolvers/project_resolver_spec.rb index 72a01b1c574..d0661c27b95 100644 --- a/spec/graphql/resolvers/project_resolver_spec.rb +++ b/spec/graphql/resolvers/project_resolver_spec.rb @@ -28,8 +28,8 @@ RSpec.describe Resolvers::ProjectResolver do end it 'does not increase complexity depending on number of load limits' do - field1 = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100) - field2 = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 1) + field1 = Types::BaseField.new(name: 'test', type: GraphQL::Types::String, resolver_class: described_class, null: false, max_page_size: 100) + field2 = Types::BaseField.new(name: 'test', type: GraphQL::Types::String, resolver_class: described_class, null: false, max_page_size: 1) expect(field1.to_graphql.complexity.call({}, {}, 1)).to eq 2 expect(field2.to_graphql.complexity.call({}, {}, 1)).to eq 2 diff --git a/spec/graphql/resolvers/terraform/states_resolver_spec.rb b/spec/graphql/resolvers/terraform/states_resolver_spec.rb index 91d48cd782b..012c74ce398 100644 --- a/spec/graphql/resolvers/terraform/states_resolver_spec.rb +++ b/spec/graphql/resolvers/terraform/states_resolver_spec.rb @@ -43,7 +43,8 @@ RSpec.describe Resolvers::Terraform::StatesResolver.single do it do expect(subject).to be_present - expect(subject.type.to_s).to eq('String!') + expect(subject.type).to be_kind_of GraphQL::Schema::NonNull + expect(subject.type.unwrap).to eq GraphQL::Types::String expect(subject.description).to be_present end end diff --git a/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb b/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb index cc855bbcb53..70f06b58a65 100644 --- a/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb +++ b/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Resolvers::UserDiscussionsCountResolver do let_it_be(:private_discussion) { create_list(:discussion_note_on_issue, 3, noteable: private_issue, project: private_project) } specify do - expect(described_class).to have_nullable_graphql_type(GraphQL::INT_TYPE) + expect(described_class).to have_nullable_graphql_type(GraphQL::Types::Int) end context 'when counting discussions from a public issue' do diff --git a/spec/graphql/resolvers/user_notes_count_resolver_spec.rb b/spec/graphql/resolvers/user_notes_count_resolver_spec.rb index 6cf23a2f57f..bc173b2a166 100644 --- a/spec/graphql/resolvers/user_notes_count_resolver_spec.rb +++ b/spec/graphql/resolvers/user_notes_count_resolver_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Resolvers::UserNotesCountResolver do let_it_be(:private_project) { create(:project, :repository, :private) } specify do - expect(described_class).to have_nullable_graphql_type(GraphQL::INT_TYPE) + expect(described_class).to have_nullable_graphql_type(GraphQL::Types::Int) end context 'when counting notes from an issue' do diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index bf533ca7034..7e3f665a99c 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -472,4 +472,23 @@ RSpec.describe ApplicationHelper do allow(helper.controller).to receive(method_name).and_return(value) end end + + describe '#gitlab_ui_form_for' do + let_it_be(:user) { build(:user) } + + before do + allow(helper).to receive(:users_path).and_return('/root') + allow(helper).to receive(:form_for).and_call_original + end + + it 'adds custom form builder to options and calls `form_for`' do + options = { html: { class: 'foo-bar' } } + expected_options = options.merge({ builder: ::Gitlab::FormBuilders::GitlabUiFormBuilder, url: '/root' }) + + expect do |b| + helper.gitlab_ui_form_for(user, options, &b) + end.to yield_with_args(::Gitlab::FormBuilders::GitlabUiFormBuilder) + expect(helper).to have_received(:form_for).with(user, expected_options) + end + end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 19c2e34a0f0..f5ed33ab114 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -2851,7 +2851,7 @@ module Gitlab YAML end - it_behaves_like 'returns errors', 'The pipeline has circular dependencies.' + it_behaves_like 'returns errors', 'The pipeline has circular dependencies' end end diff --git a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb new file mode 100644 index 00000000000..a46846e9820 --- /dev/null +++ b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do + let_it_be(:user) { build(:user) } + let_it_be(:fake_template) do + Object.new.tap do |template| + template.extend ActionView::Helpers::FormHelper + template.extend ActionView::Helpers::FormOptionsHelper + template.extend ActionView::Helpers::TagHelper + template.extend ActionView::Context + end + end + + let_it_be(:form_builder) { described_class.new(:user, user, fake_template, {}) } + + describe '#gitlab_ui_checkbox_component' do + let(:optional_args) { {} } + + subject(:checkbox_html) { form_builder.gitlab_ui_checkbox_component(:view_diffs_file_by_file, "Show one file at a time on merge request's Changes tab", **optional_args) } + + context 'without optional arguments' do + it 'renders correct html' do + expected_html = <<~EOS + <div class="gl-form-checkbox custom-control custom-checkbox"> + <input name="user[view_diffs_file_by_file]" type="hidden" value="0" /> + <input class="custom-control-input" type="checkbox" value="1" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" /> + <label class="custom-control-label" for="user_view_diffs_file_by_file"> + Show one file at a time on merge request's Changes tab + </label> + </div> + EOS + + expect(checkbox_html).to eq(html_strip_whitespace(expected_html)) + end + end + + context 'with optional arguments' do + let(:optional_args) do + { + help_text: 'Instead of all the files changed, show only one file at a time.', + checkbox_options: { class: 'checkbox-foo-bar' }, + label_options: { class: 'label-foo-bar' }, + checked_value: '3', + unchecked_value: '1' + } + end + + it 'renders help text' do + expected_html = <<~EOS + <div class="gl-form-checkbox custom-control custom-checkbox"> + <input name="user[view_diffs_file_by_file]" type="hidden" value="1" /> + <input class="custom-control-input checkbox-foo-bar" type="checkbox" value="3" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" /> + <label class="custom-control-label label-foo-bar" for="user_view_diffs_file_by_file"> + <span>Show one file at a time on merge request's Changes tab</span> + <p class="help-text">Instead of all the files changed, show only one file at a time.</p> + </label> + </div> + EOS + + expect(checkbox_html).to eq(html_strip_whitespace(expected_html)) + end + + it 'passes arguments to `check_box` method' do + allow(fake_template).to receive(:check_box).and_return('') + + checkbox_html + + expect(fake_template).to have_received(:check_box).with(:user, :view_diffs_file_by_file, { class: %w(custom-control-input checkbox-foo-bar), object: user }, '3', '1') + end + + it 'passes arguments to `label` method' do + allow(fake_template).to receive(:label).and_return('') + + checkbox_html + + expect(fake_template).to have_received(:label).with(:user, :view_diffs_file_by_file, { class: %w(custom-control-label label-foo-bar), object: user }) + end + end + end + + private + + def html_strip_whitespace(html) + html.lines.map(&:strip).join('') + end +end diff --git a/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb index bfcd4994995..dbd6cb243f6 100644 --- a/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb @@ -77,7 +77,7 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeRestorer do let(:group) { create(:group) } let(:shared) { Gitlab::ImportExport::Shared.new(group) } let(:group_tree_restorer) { described_class.new(user: importer_user, shared: shared, group: group, group_hash: nil) } - let(:group_json) { ActiveSupport::JSON.decode(IO.read(File.join(shared.export_path, 'group.json'))) } + let(:group_json) { Gitlab::Json.parse(IO.read(File.join(shared.export_path, 'group.json'))) } shared_examples 'excluded attributes' do excluded_attributes = %w[ diff --git a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb index d2153221e8f..b67d42d1b71 100644 --- a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb @@ -111,7 +111,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do let(:shared) { Gitlab::ImportExport::Shared.new(group) } let(:group_tree_restorer) { described_class.new(user: importer_user, shared: shared, group: group) } let(:exported_file) { File.join(shared.export_path, 'tree/groups/4352.json') } - let(:group_json) { ActiveSupport::JSON.decode(IO.read(exported_file)) } + let(:group_json) { Gitlab::Json.parse(IO.read(exported_file)) } shared_examples 'excluded attributes' do excluded_attributes = %w[ diff --git a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb index 9c6d2708607..90966cb4915 100644 --- a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb +++ b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb @@ -86,7 +86,7 @@ RSpec.describe 'Test coverage of the Project Import' do end def relations_from_json(json_file) - json = ActiveSupport::JSON.decode(IO.read(json_file)) + json = Gitlab::Json.parse(IO.read(json_file)) [].tap {|res| gather_relations({ project: json }, res, [])} .map {|relation_names| relation_names.join('.')} diff --git a/spec/lib/gitlab/json_cache_spec.rb b/spec/lib/gitlab/json_cache_spec.rb index 8265c3449bb..7899d01b475 100644 --- a/spec/lib/gitlab/json_cache_spec.rb +++ b/spec/lib/gitlab/json_cache_spec.rb @@ -130,7 +130,7 @@ RSpec.describe Gitlab::JsonCache do .with(expanded_key) .and_return(nil) - expect(ActiveSupport::JSON).not_to receive(:decode) + expect(Gitlab::Json).not_to receive(:parse) expect(cache.read(key)).to be_nil end @@ -140,7 +140,7 @@ RSpec.describe Gitlab::JsonCache do .with(expanded_key) .and_return(true) - expect(ActiveSupport::JSON).to receive(:decode).with("true").and_call_original + expect(Gitlab::Json).to receive(:parse).with("true").and_call_original expect(cache.read(key, BroadcastMessage)).to eq(true) end end @@ -151,7 +151,7 @@ RSpec.describe Gitlab::JsonCache do .with(expanded_key) .and_return(false) - expect(ActiveSupport::JSON).to receive(:decode).with("false").and_call_original + expect(Gitlab::Json).to receive(:parse).with("false").and_call_original expect(cache.read(key, BroadcastMessage)).to eq(false) end end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 3fb683ea0fa..2a91dae32b8 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -267,7 +267,7 @@ RSpec.describe 'Git HTTP requests' do it "responds to pulls with the wiki's repo" do download(path) do |response| - json_body = ActiveSupport::JSON.decode(response.body) + json_body = Gitlab::Json.parse(response.body) expect(json_body['Repository']['relative_path']).to eq(wiki.repository.relative_path) end @@ -1584,7 +1584,7 @@ RSpec.describe 'Git HTTP requests' do it "responds to pulls with the wiki's repo" do download(path) do |response| - json_body = ActiveSupport::JSON.decode(response.body) + json_body = Gitlab::Json.parse(response.body) expect(json_body['Repository']['relative_path']).to eq(wiki.repository.relative_path) end diff --git a/spec/rubocop/cop/gitlab/json_spec.rb b/spec/rubocop/cop/gitlab/json_spec.rb index 66b2c675e80..7998f26da4e 100644 --- a/spec/rubocop/cop/gitlab/json_spec.rb +++ b/spec/rubocop/cop/gitlab/json_spec.rb @@ -6,7 +6,7 @@ require_relative '../../../../rubocop/cop/gitlab/json' RSpec.describe RuboCop::Cop::Gitlab::Json do subject(:cop) { described_class.new } - context 'when JSON is called' do + context 'when ::JSON is called' do it 'registers an offense' do expect_offense(<<~RUBY) class Foo @@ -18,4 +18,17 @@ RSpec.describe RuboCop::Cop::Gitlab::Json do RUBY end end + + context 'when ActiveSupport::JSON is called' do + it 'registers an offense' do + expect_offense(<<~RUBY) + class Foo + def bar + ActiveSupport::JSON.parse('{ "foo": "bar" }') + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid calling `JSON` directly. [...] + end + end + RUBY + end + end end diff --git a/spec/services/packages/debian/generate_distribution_key_service_spec.rb b/spec/services/packages/debian/generate_distribution_key_service_spec.rb index b31830c2d3b..f82d577f071 100644 --- a/spec/services/packages/debian/generate_distribution_key_service_spec.rb +++ b/spec/services/packages/debian/generate_distribution_key_service_spec.rb @@ -3,33 +3,21 @@ require 'spec_helper' RSpec.describe Packages::Debian::GenerateDistributionKeyService do - let_it_be(:user) { create(:user) } - let(:params) { {} } - subject { described_class.new(current_user: user, params: params) } + subject { described_class.new(params: params) } let(:response) { subject.execute } - context 'with a user' do - it 'returns an Hash', :aggregate_failures do - expect(GPGME::Ctx).to receive(:new).with(armor: true, offline: true).and_call_original - expect(User).to receive(:random_password).with(no_args).and_call_original - - expect(response).to be_a Hash - expect(response.keys).to contain_exactly(:private_key, :public_key, :fingerprint, :passphrase) - expect(response[:private_key]).to start_with('-----BEGIN PGP PRIVATE KEY BLOCK-----') - expect(response[:public_key]).to start_with('-----BEGIN PGP PUBLIC KEY BLOCK-----') - expect(response[:fingerprint].length).to eq(40) - expect(response[:passphrase].length).to be > 10 - end - end - - context 'without a user' do - let(:user) { nil } + it 'returns an Hash', :aggregate_failures do + expect(GPGME::Ctx).to receive(:new).with(armor: true, offline: true).and_call_original + expect(User).to receive(:random_password).with(no_args).and_call_original - it 'raises an ArgumentError' do - expect { response }.to raise_error(ArgumentError, 'Please provide a user') - end + expect(response).to be_a Hash + expect(response.keys).to contain_exactly(:private_key, :public_key, :fingerprint, :passphrase) + expect(response[:private_key]).to start_with('-----BEGIN PGP PRIVATE KEY BLOCK-----') + expect(response[:public_key]).to start_with('-----BEGIN PGP PUBLIC KEY BLOCK-----') + expect(response[:fingerprint].length).to eq(40) + expect(response[:passphrase].length).to be > 10 end end diff --git a/spec/services/packages/debian/sign_distribution_service_spec.rb b/spec/services/packages/debian/sign_distribution_service_spec.rb new file mode 100644 index 00000000000..b1096ba235f --- /dev/null +++ b/spec/services/packages/debian/sign_distribution_service_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Debian::SignDistributionService do + let_it_be(:group) { create(:group, :public) } + + let(:content) { FFaker::Lorem.paragraph } + let(:params) { {} } + let(:service) { described_class.new(distribution, content, params: params) } + + shared_examples 'Sign Distribution' do |container_type, detach: false| + context "for #{container_type} detach=#{detach}" do + let(:params) { { detach: detach } } + + if container_type == :group + let_it_be(:distribution) { create('debian_group_distribution', container: group) } + else + let_it_be(:project) { create(:project, group: group) } + let_it_be(:distribution) { create('debian_project_distribution', container: project) } + end + + describe '#execute' do + subject { service.execute } + + context 'without an existing key' do + it 'raises ArgumentError', :aggregate_failures do + expect { subject } + .to raise_error(ArgumentError, 'distribution key is missing') + end + end + + context 'with an existing key' do + let!(:key) { create("debian_#{container_type}_distribution_key", distribution: distribution)} + + it 'returns the content signed', :aggregate_failures do + expect(Packages::Debian::GenerateDistributionKeyService).not_to receive(:new) + + key_class = "Packages::Debian::#{container_type.capitalize}DistributionKey".constantize + + expect { subject } + .to not_change { key_class.count } + + if detach + expect(subject).to start_with("-----BEGIN PGP SIGNATURE-----\n") + else + expect(subject).to start_with("-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\n#{content}\n-----BEGIN PGP SIGNATURE-----\n") + end + + expect(subject).to end_with("\n-----END PGP SIGNATURE-----\n") + end + end + end + end + end + + it_behaves_like 'Sign Distribution', :project + it_behaves_like 'Sign Distribution', :project, detach: true + it_behaves_like 'Sign Distribution', :group + it_behaves_like 'Sign Distribution', :group, detach: true +end diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb index 5fb6af99b79..1aa20dab6f8 100644 --- a/spec/support/import_export/common_util.rb +++ b/spec/support/import_export/common_util.rb @@ -83,7 +83,7 @@ module ImportExport path = File.join(dir_path, "#{exportable_path}.json") return unless File.exist?(path) - ActiveSupport::JSON.decode(IO.read(path)) + Gitlab::Json.parse(IO.read(path)) end def consume_relations(dir_path, exportable_path, key) @@ -93,7 +93,7 @@ module ImportExport relations = [] File.foreach(path) do |line| - json = ActiveSupport::JSON.decode(line) + json = Gitlab::Json.parse(line) relations << json end @@ -101,7 +101,7 @@ module ImportExport end def project_json(filename) - ActiveSupport::JSON.decode(IO.read(filename)) + Gitlab::Json.parse(IO.read(filename)) end end end diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb index 43a6fcc2adc..9fa95613d1c 100644 --- a/spec/views/admin/dashboard/index.html.haml_spec.rb +++ b/spec/views/admin/dashboard/index.html.haml_spec.rb @@ -52,4 +52,12 @@ RSpec.describe 'admin/dashboard/index.html.haml' do expect(rendered).not_to have_content "Maximum Users" expect(rendered).not_to have_content "Users over License" end + + it 'links to the GitLab Changelog' do + stub_application_setting(version_check_enabled: true) + + render + + expect(rendered).to have_link(href: 'https://gitlab.com/gitlab-org/gitlab/-/blob/master/CHANGELOG.md') + end end |