diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
index d7219e3a91c349f15a86717351cd125666daa77d..2b85a787c933a9ff3488277dbe29a7aaf1bef9c7 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
@@ -1,5 +1,5 @@
@@ -135,7 +154,7 @@ export default {
:fields="$options.fields"
show-empty
stacked="md"
- class="mb-3"
+ class="gl-mb-5!"
:aria-label="$options.i18n.settingBlockTitle"
:busy="isLoadingPackageProtectionRules"
>
@@ -143,6 +162,16 @@ export default {
+
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql
index e0a072b93e4c0f9b8d53830b1062011e234e7db1..c90c00b4d1a328ec72695f4b43a66cead13dda25 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql
@@ -1,13 +1,24 @@
-query getProjectPackageProtectionRules($projectPath: ID!, $first: Int) {
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
+query getProjectPackageProtectionRules(
+ $projectPath: ID!
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+) {
project(fullPath: $projectPath) {
id
- packagesProtectionRules(first: $first) {
+ packagesProtectionRules(first: $first, last: $last, after: $after, before: $before) {
nodes {
id
packageNamePattern
packageType
pushProtectedUpToAccessLevel
}
+ pageInfo {
+ ...PageInfo
+ }
}
}
}
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
index 27b7ec44db322090ed82355f550473d55ceaed1a..507711a0764c4e67cac4d4091368855185377dea 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
@@ -1,4 +1,4 @@
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -60,7 +60,7 @@ describe('Packages protection rules project settings', () => {
expect(findTable().exists()).toBe(true);
});
- describe('table package protection rules', () => {
+ describe('table "package protection rules"', () => {
it('renders table with packages protection rules', async () => {
createComponent({ mountFn: mountExtended });
@@ -68,11 +68,13 @@ describe('Packages protection rules project settings', () => {
expect(findTable().exists()).toBe(true);
- packagesProtectionRulesData.forEach((protectionRule, i) => {
- expect(findTableRow(i).text()).toContain(protectionRule.packageNamePattern);
- expect(findTableRow(i).text()).toContain(protectionRule.packageType);
- expect(findTableRow(i).text()).toContain(protectionRule.pushProtectedUpToAccessLevel);
- });
+ packagesProtectionRuleQueryPayload().data.project.packagesProtectionRules.nodes.forEach(
+ (protectionRule, i) => {
+ expect(findTableRow(i).text()).toContain(protectionRule.packageNamePattern);
+ expect(findTableRow(i).text()).toContain(protectionRule.packageType);
+ expect(findTableRow(i).text()).toContain(protectionRule.pushProtectedUpToAccessLevel);
+ },
+ );
});
it('displays table in busy state and shows loading icon inside table', async () => {
@@ -88,6 +90,123 @@ describe('Packages protection rules project settings', () => {
expect(findTableLoadingIcon().exists()).toBe(false);
expect(findTable().attributes('aria-busy')).toBe('false');
});
+
+ it('calls graphql api query', () => {
+ const resolver = jest.fn().mockResolvedValue(packagesProtectionRuleQueryPayload());
+ createComponent({ resolver });
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ projectPath: defaultProvidedValues.projectPath }),
+ );
+ });
+
+ describe('table pagination', () => {
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+
+ it('renders pagination', async () => {
+ createComponent({ mountFn: mountExtended });
+
+ await waitForPromises();
+
+ expect(findPagination().exists()).toBe(true);
+ expect(findPagination().props()).toMatchObject({
+ endCursor: '10',
+ startCursor: '0',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ });
+ });
+
+ it('calls initial graphql api query with pagination information', () => {
+ const resolver = jest.fn().mockResolvedValue(packagesProtectionRuleQueryPayload());
+ createComponent({ resolver });
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({
+ projectPath: defaultProvidedValues.projectPath,
+ first: 10,
+ }),
+ );
+ });
+
+ describe('when button "Previous" is clicked', () => {
+ const resolver = jest
+ .fn()
+ .mockResolvedValueOnce(
+ packagesProtectionRuleQueryPayload({
+ nodes: packagesProtectionRulesData.slice(10),
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: true,
+ startCursor: '10',
+ endCursor: '16',
+ },
+ }),
+ )
+ .mockResolvedValueOnce(packagesProtectionRuleQueryPayload());
+
+ const findPaginationButtonPrev = () =>
+ extendedWrapper(findPagination()).findByRole('button', { name: 'Previous' });
+
+ beforeEach(async () => {
+ createComponent({ mountFn: mountExtended, resolver });
+
+ await waitForPromises();
+
+ findPaginationButtonPrev().trigger('click');
+ });
+
+ it('sends a second graphql api query with new pagination params', () => {
+ expect(resolver).toHaveBeenCalledTimes(2);
+ expect(resolver).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ before: '10',
+ last: 10,
+ projectPath: 'path',
+ }),
+ );
+ });
+ });
+
+ describe('when button "Next" is clicked', () => {
+ const resolver = jest
+ .fn()
+ .mockResolvedValueOnce(packagesProtectionRuleQueryPayload())
+ .mockResolvedValueOnce(
+ packagesProtectionRuleQueryPayload({
+ nodes: packagesProtectionRulesData.slice(10),
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: '1',
+ endCursor: '10',
+ },
+ }),
+ );
+
+ const findPaginationButtonNext = () =>
+ extendedWrapper(findPagination()).findByRole('button', { name: 'Next' });
+
+ beforeEach(async () => {
+ createComponent({ mountFn: mountExtended, resolver });
+
+ await waitForPromises();
+
+ findPaginationButtonNext().trigger('click');
+ });
+
+ it('sends a second graphql api query with new pagination params', () => {
+ expect(resolver).toHaveBeenCalledTimes(2);
+ expect(resolver).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ after: '10',
+ first: 10,
+ projectPath: 'path',
+ }),
+ );
+ });
+ });
+ });
});
it('does not initially render package protection form', async () => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
index a8133c0ace6a1a4b1f31edb869d412727d1ceb71..e49bf8c6131e6e532d65fc7716f386911c31b346 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
@@ -81,18 +81,12 @@ export const packagesCleanupPolicyMutationPayload = ({ override, errors = [] } =
});
export const packagesProtectionRulesData = [
- {
- id: `gid://gitlab/Packages::Protection::Rule/14`,
- packageNamePattern: `@flight/flight-maintainer-14-*`,
+ ...Array.from(Array(15)).map((_e, i) => ({
+ id: `gid://gitlab/Packages::Protection::Rule/${i}`,
+ packageNamePattern: `@flight/flight-maintainer-${i}-*`,
packageType: 'NPM',
pushProtectedUpToAccessLevel: 'MAINTAINER',
- },
- {
- id: `gid://gitlab/Packages::Protection::Rule/15`,
- packageNamePattern: `@flight/flight-maintainer-15-*`,
- packageType: 'NPM',
- pushProtectedUpToAccessLevel: 'MAINTAINER',
- },
+ })),
{
id: 'gid://gitlab/Packages::Protection::Rule/16',
packageNamePattern: '@flight/flight-owner-16-*',
@@ -101,12 +95,22 @@ export const packagesProtectionRulesData = [
},
];
-export const packagesProtectionRuleQueryPayload = ({ override, errors = [] } = {}) => ({
+export const packagesProtectionRuleQueryPayload = ({
+ errors = [],
+ nodes = packagesProtectionRulesData.slice(0, 10),
+ pageInfo = {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: '0',
+ endCursor: '10',
+ },
+} = {}) => ({
data: {
project: {
id: '1',
packagesProtectionRules: {
- nodes: override || packagesProtectionRulesData,
+ nodes,
+ pageInfo: { __typename: 'PageInfo', ...pageInfo },
},
errors,
},