diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/block_group_branch_modification_setting.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/block_group_branch_modification_setting.vue index d990ee96dc7ca43414a00d4eb00b26de33dcae2a..33c0c18464ce84854bdb7aaa1c51436069c9f2bf 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/block_group_branch_modification_setting.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/block_group_branch_modification_setting.vue @@ -3,7 +3,7 @@ import { GlSprintf } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { TYPENAME_GROUP } from '~/graphql_shared/constants'; -import getGroupsByIds from 'ee/security_orchestration/graphql/queries/get_groups_by_ids.qyery.graphql'; +import getGroupsByIds from 'ee/security_orchestration/graphql/queries/get_groups_by_ids.query.graphql'; export default { name: 'BlockGroupBranchModificationSetting', diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/settings/block_group_branch_modification.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/settings/block_group_branch_modification.vue index e1ed212c1172c76475944b9c735c5e1b7fa619d9..6f1872f65c95c069e4d6d84d24323862c6a22448 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/settings/block_group_branch_modification.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/settings/block_group_branch_modification.vue @@ -7,13 +7,14 @@ import { GlSprintf, GlTooltipDirective, } from '@gitlab/ui'; +import produce from 'immer'; import { createAlert } from '~/alert'; import { helpPagePath } from '~/helpers/help_page_helper'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { getSelectedOptionsText } from '~/lib/utils/listbox_helpers'; import { TYPENAME_GROUP } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; -import getGroupsByIds from 'ee/security_orchestration/graphql/queries/get_groups_by_ids.qyery.graphql'; +import getGroupsByIds from 'ee/security_orchestration/graphql/queries/get_groups_by_ids.query.graphql'; import { searchInItemsProperties } from '~/lib/utils/search_utils'; import { __, s__ } from '~/locale'; import { @@ -41,6 +42,7 @@ export default { }, update(data) { const groups = data.groups?.nodes?.map(createGroupObject) || []; + this.pageInfo = data.groups?.pageInfo || {}; return this.joinUniqueGroups(groups); }, async result() { @@ -82,6 +84,7 @@ export default { selectedExceptionType: this.exceptions.length ? EXCEPT_GROUPS : WITHOUT_EXCEPTIONS, searchValue: '', searching: false, + pageInfo: {}, }; }, computed: { @@ -124,6 +127,9 @@ export default { maxOptionsShown: 2, }); }, + hasNextPage() { + return this.pageInfo.hasNextPage; + }, }, watch: { enabled(value) { @@ -181,6 +187,27 @@ export default { emitChangeEvent(value) { this.$emit('change', value); }, + async loadMoreGroups() { + if (!this.hasNextPage) return []; + try { + return await this.$apollo.queries.groups.fetchMore({ + variables: { + search: this.searchValue, + after: this.pageInfo.endCursor, + }, + updateQuery(previousResult, { fetchMoreResult }) { + return produce(fetchMoreResult, (draftData) => { + draftData.groups.nodes = [ + ...previousResult.groups.nodes, + ...fetchMoreResult.groups.nodes, + ]; + }); + }, + }); + } catch { + return []; + } + }, }, GROUP_PROTECTED_BRANCHES_DOCS: helpPagePath('user/project/repository/branches/protected', { @@ -216,9 +243,11 @@ export default { multiple searchable :items="filteredGroups" + :infinite-scroll="hasNextPage" :loading="loading" :selected="exceptionIds" :toggle-text="toggleText" + @bottom-reached="loadMoreGroups" @search="handleSearch" @select="updateGroupExceptionValue" > diff --git a/ee/app/assets/javascripts/security_orchestration/graphql/queries/get_groups_by_ids.qyery.graphql b/ee/app/assets/javascripts/security_orchestration/graphql/queries/get_groups_by_ids.query.graphql similarity index 85% rename from ee/app/assets/javascripts/security_orchestration/graphql/queries/get_groups_by_ids.qyery.graphql rename to ee/app/assets/javascripts/security_orchestration/graphql/queries/get_groups_by_ids.query.graphql index 1e8ea046ef4094121395a714d428d28a536c66c5..1b0b79e1c58155d65473edead34a0fd8a9976eb4 100644 --- a/ee/app/assets/javascripts/security_orchestration/graphql/queries/get_groups_by_ids.qyery.graphql +++ b/ee/app/assets/javascripts/security_orchestration/graphql/queries/get_groups_by_ids.query.graphql @@ -1,6 +1,6 @@ #import "~/graphql_shared/fragments/page_info.fragment.graphql" -query getGroupsByIds($ids: [ID!], $search: String, $topLevelOnly: Boolean) { - groups(ids: $ids, search: $search, topLevelOnly: $topLevelOnly) { +query getGroupsByIds($ids: [ID!], $search: String, $topLevelOnly: Boolean, $after: String = "") { + groups(ids: $ids, search: $search, topLevelOnly: $topLevelOnly, after: $after) { nodes { id name diff --git a/ee/spec/frontend/security_orchestration/components/policy_drawer/scan_result/block_group_branch_modification_setting_spec.js b/ee/spec/frontend/security_orchestration/components/policy_drawer/scan_result/block_group_branch_modification_setting_spec.js index cd2e28c0c4de30357e821ef93cac6689441b30af..50fef779469246a2dbf663b91c78787f314be972 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_drawer/scan_result/block_group_branch_modification_setting_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_drawer/scan_result/block_group_branch_modification_setting_spec.js @@ -8,7 +8,7 @@ import { createMockGroups } from 'ee_jest/security_orchestration/mocks/mock_data import { convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPENAME_GROUP } from '~/graphql_shared/constants'; import createMockApollo from 'helpers/mock_apollo_helper'; -import getGroupsByIds from 'ee/security_orchestration/graphql/queries/get_groups_by_ids.qyery.graphql'; +import getGroupsByIds from 'ee/security_orchestration/graphql/queries/get_groups_by_ids.query.graphql'; describe('BlockGroupBranchModificationSetting', () => { let wrapper; @@ -52,6 +52,7 @@ describe('BlockGroupBranchModificationSetting', () => { expect(requestHandler).toHaveBeenCalledWith({ ids: exceptions.map(({ id }) => convertToGraphQLId(TYPENAME_GROUP, id)), + after: '', }); expect(findExceptions()).toHaveLength(2); expect(findExceptions().at(0).text()).toBe(groups[0].fullName); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/settings/block_group_branch_modification_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/settings/block_group_branch_modification_spec.js index b82af83b326c4461babb8f9e910d227fcc995669..b3fe490bfb2d38677d1ca4c9323be2e904f44393 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/settings/block_group_branch_modification_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/settings/block_group_branch_modification_spec.js @@ -10,7 +10,7 @@ import { } from 'ee/security_orchestration/components/policy_editor/scan_result/lib/settings'; import BlockGroupBranchModification from 'ee/security_orchestration/components/policy_editor/scan_result/settings/block_group_branch_modification.vue'; import { createMockGroups } from 'ee_jest/security_orchestration/mocks/mock_data'; -import getGroupsByIds from 'ee/security_orchestration/graphql/queries/get_groups_by_ids.qyery.graphql'; +import getGroupsByIds from 'ee/security_orchestration/graphql/queries/get_groups_by_ids.query.graphql'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPENAME_GROUP } from '~/graphql_shared/constants'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -21,12 +21,20 @@ describe('BlockGroupBranchModification', () => { let wrapper; let requestHandler; - const defaultHandler = (nodes = createMockGroups()) => + const defaultPageInfo = { + __typename: 'PageInfo', + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }; + + const defaultHandler = (nodes = createMockGroups(), pageInfo = defaultPageInfo) => jest.fn().mockResolvedValue({ data: { groups: { nodes, - pageInfo: {}, + pageInfo, }, }, }); @@ -67,7 +75,7 @@ describe('BlockGroupBranchModification', () => { }); it('renders when enabled', () => { - createComponent({ enabled: true }); + createComponent({ propsData: { enabled: true } }); expect(findLink().attributes('href')).toBe( '/help/user/project/repository/branches/protected#for-all-projects-in-a-group', ); @@ -93,7 +101,7 @@ describe('BlockGroupBranchModification', () => { }); it('retrieves top-level groups', () => { - expect(requestHandler).toHaveBeenCalledWith({ topLevelOnly: true, search: '' }); + expect(requestHandler).toHaveBeenCalledWith({ topLevelOnly: true, search: '', after: '' }); }); it('renders the except selection dropdown', () => { @@ -160,7 +168,7 @@ describe('BlockGroupBranchModification', () => { it('searches for groups', async () => { createComponent({ propsData: { enabled: true, exceptions: [{ id: 1 }, { id: 2 }] } }); await findExceptionsDropdown().vm.$emit('search', 'git'); - expect(requestHandler).toHaveBeenCalledWith({ search: 'git', topLevelOnly: true }); + expect(requestHandler).toHaveBeenCalledWith({ search: 'git', topLevelOnly: true, after: '' }); }); }); @@ -187,11 +195,42 @@ describe('BlockGroupBranchModification', () => { await waitForPromises(); expect(requestHandler).toHaveBeenCalledTimes(2); - expect(requestHandler).toHaveBeenNthCalledWith(1, { topLevelOnly: true, search: '' }); + expect(requestHandler).toHaveBeenNthCalledWith(1, { + topLevelOnly: true, + search: '', + after: '', + }); expect(requestHandler).toHaveBeenNthCalledWith(2, { + after: '', topLevelOnly: true, ids: [7, 8].map((id) => convertToGraphQLId(TYPENAME_GROUP, id)), }); }); }); + + describe('infinite scroll', () => { + it('does not make a query to fetch more groups when there is no next page', async () => { + createComponent({ propsData: { enabled: true, exceptions: [{ id: 1 }, { id: 2 }] } }); + await waitForPromises(); + findExceptionsDropdown().vm.$emit('bottom-reached'); + + expect(requestHandler).toHaveBeenCalledTimes(1); + }); + + it('makes a query to fetch more groups when there is a next page', async () => { + createComponent({ + propsData: { enabled: true, exceptions: [{ id: 1 }, { id: 2 }] }, + handler: defaultHandler(createMockGroups(), { ...defaultPageInfo, hasNextPage: true }), + }); + await waitForPromises(); + findExceptionsDropdown().vm.$emit('bottom-reached'); + + expect(requestHandler).toHaveBeenCalledTimes(2); + expect(requestHandler).toHaveBeenNthCalledWith(2, { + after: null, + topLevelOnly: true, + search: '', + }); + }); + }); });