From b1ecd84896519371262e5d32fe9c70e6578a35e8 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Sat, 2 Mar 2024 19:23:13 +0100 Subject: [PATCH 1/3] feat: Protected containers: Protection rules in project settings ui - Adds frontend ui for the container protection rule in project settings - Hide project setting for container protection rules when relevant feature flag is disabled Changelog: added --- .../components/container_protection_rules.vue | 217 ++++++++++++++ .../components/registry_settings_app.vue | 8 + ...t_container_protection_rules.query.graphql | 24 ++ .../packages_and_registries_controller.rb | 5 + locale/gitlab.pot | 15 + .../container_protection_rules_spec.js | 277 ++++++++++++++++++ .../components/registry_settings_app_spec.js | 23 +- .../settings/project/settings/mock_data.js | 37 +++ ...packages_and_registries_controller_spec.rb | 18 ++ 9 files changed, 623 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue create mode 100644 app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_container_protection_rules.query.graphql create mode 100644 spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rules_spec.js diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue new file mode 100644 index 00000000000000..467e1c614789c4 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue @@ -0,0 +1,217 @@ + + + diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue index 8e4c50b199be1a..2b620ece677545 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue @@ -7,12 +7,14 @@ import { UPDATE_SETTINGS_SUCCESS_MESSAGE, } from '~/packages_and_registries/settings/project/constants'; import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue'; +import ContainerProtectionRules from '~/packages_and_registries/settings/project/components/container_protection_rules.vue'; import PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { ContainerExpirationPolicy, + ContainerProtectionRules, DependencyProxyPackagesSettings: () => import( 'ee_component/packages_and_registries/settings/project/components/dependency_proxy_packages_settings.vue' @@ -40,6 +42,11 @@ export default { showProtectedPackagesSettings() { return this.showPackageRegistrySettings && this.glFeatures.packagesProtectedPackages; }, + showProtectedContainersSettings() { + return ( + this.showContainerRegistrySettings && this.glFeatures.containerRegistryProtectedContainers + ); + }, }, mounted() { this.checkAlert(); @@ -71,6 +78,7 @@ export default { + diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_container_protection_rules.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_container_protection_rules.query.graphql new file mode 100644 index 00000000000000..29c9c55bbb71f3 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_container_protection_rules.query.graphql @@ -0,0 +1,24 @@ +#import "~/graphql_shared/fragments/page_info.fragment.graphql" + +query getProjectContainerProtectionRules( + $projectPath: ID! + $first: Int + $last: Int + $after: String + $before: String +) { + project(fullPath: $projectPath) { + id + containerRegistryProtectionRules(first: $first, last: $last, after: $after, before: $before) { + nodes { + id + repositoryPathPattern + pushProtectedUpToAccessLevel + deleteProtectedUpToAccessLevel + } + pageInfo { + ...PageInfo + } + } + } +} diff --git a/app/controllers/projects/settings/packages_and_registries_controller.rb b/app/controllers/projects/settings/packages_and_registries_controller.rb index 5c352866c8dfd1..4a3235bf9f30eb 100644 --- a/app/controllers/projects/settings/packages_and_registries_controller.rb +++ b/app/controllers/projects/settings/packages_and_registries_controller.rb @@ -8,6 +8,7 @@ class PackagesAndRegistriesController < Projects::ApplicationController before_action :authorize_admin_project! before_action :packages_and_registries_settings_enabled! before_action :set_feature_flag_packages_protected_packages, only: :show + before_action :set_feature_flag_container_registry_protected_containers, only: :show feature_category :package_registry urgency :low @@ -35,6 +36,10 @@ def registry_settings_enabled! def set_feature_flag_packages_protected_packages push_frontend_feature_flag(:packages_protected_packages, project) end + + def set_feature_flag_container_registry_protected_containers + push_frontend_feature_flag(:container_registry_protected_containers, project) + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 58a0ed42db8a8c..edeace0875a255 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13715,6 +13715,9 @@ msgstr "" msgid "ContainerRegistry|Container Registry" msgstr "" +msgid "ContainerRegistry|Container protection rules" +msgstr "" + msgid "ContainerRegistry|Copy build command" msgstr "" @@ -13736,6 +13739,9 @@ msgstr "" msgid "ContainerRegistry|Delete image repository?" msgstr "" +msgid "ContainerRegistry|Delete protected up to access level" +msgstr "" + msgid "ContainerRegistry|Delete selected tags" msgstr "" @@ -13826,6 +13832,9 @@ msgstr "" msgid "ContainerRegistry|Push an image" msgstr "" +msgid "ContainerRegistry|Push protected up to access level" +msgstr "" + msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage." msgstr "" @@ -13846,6 +13855,9 @@ msgstr "" msgid "ContainerRegistry|Remove these tags" msgstr "" +msgid "ContainerRegistry|Repository path pattern" +msgstr "" + msgid "ContainerRegistry|Run cleanup:" msgstr "" @@ -13966,6 +13978,9 @@ msgstr "" msgid "ContainerRegistry|We are having trouble connecting to the Container Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}." msgstr "" +msgid "ContainerRegistry|When a container is protected then only certain user roles are able to update and delete the protected container. This helps to avoid tampering with the container." +msgstr "" + msgid "ContainerRegistry|While the rename is in progress, new uploads to the container registry are blocked. Ongoing uploads may fail and need to be retried." msgstr "" diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rules_spec.js new file mode 100644 index 00000000000000..e8f9cb7d27c5a6 --- /dev/null +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rules_spec.js @@ -0,0 +1,277 @@ +import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import ContainerProtectionRules from '~/packages_and_registries/settings/project/components/container_protection_rules.vue'; +import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue'; +import ContainerProtectionRuleQuery from '~/packages_and_registries/settings/project/graphql/queries/get_container_protection_rules.query.graphql'; +import { containerProtectionRulesData, containerProtectionRuleQueryPayload } from '../mock_data'; + +Vue.use(VueApollo); + +describe('Container protection rules project settings', () => { + let wrapper; + let fakeApollo; + + const defaultProvidedValues = { + projectPath: 'path', + }; + + const $toast = { show: jest.fn() }; + + const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); + const findTable = () => extendedWrapper(wrapper.findByRole('table', /protected Container/i)); + const findTableBody = () => extendedWrapper(findTable().findAllByRole('rowgroup').at(1)); + const findTableRow = (i) => extendedWrapper(findTableBody().findAllByRole('row').at(i)); + const findTableLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findAlert = () => wrapper.findByRole('alert'); + + const mountComponent = (mountFn = mountExtended, provide = defaultProvidedValues, config) => { + wrapper = mountFn(ContainerProtectionRules, { + stubs: { + SettingsBlock, + GlModal: true, + }, + mocks: { + $toast, + }, + provide, + ...config, + }); + }; + + const createComponent = ({ + mountFn = mountExtended, + provide = defaultProvidedValues, + containerProtectionRuleQueryResolver = jest + .fn() + .mockResolvedValue(containerProtectionRuleQueryPayload()), + config = {}, + } = {}) => { + const requestHandlers = [[ContainerProtectionRuleQuery, containerProtectionRuleQueryResolver]]; + + fakeApollo = createMockApollo(requestHandlers); + + mountComponent(mountFn, provide, { + apolloProvider: fakeApollo, + ...config, + }); + }; + + it('renders the setting block with table', async () => { + createComponent(); + + await waitForPromises(); + + expect(findSettingsBlock().exists()).toBe(true); + expect(findTable().exists()).toBe(true); + }); + + describe('table "package protection rules"', () => { + it('renders table with Container protection rules', async () => { + createComponent(); + + await waitForPromises(); + + expect(findTable().exists()).toBe(true); + + containerProtectionRuleQueryPayload().data.project.containerRegistryProtectionRules.nodes.forEach( + (protectionRule, i) => { + expect(findTableRow(i).text()).toContain(protectionRule.repositoryPathPattern); + expect(findTableRow(i).text()).toContain('Maintainer'); + expect(findTableRow(i).text()).toContain('Maintainer'); + }, + ); + }); + + it('displays table in busy state and shows loading icon inside table', async () => { + createComponent(); + + expect(findTableLoadingIcon().exists()).toBe(true); + expect(findTableLoadingIcon().attributes('aria-label')).toBe('Loading'); + + expect(findTable().attributes('aria-busy')).toBe('true'); + + await waitForPromises(); + + expect(findTableLoadingIcon().exists()).toBe(false); + expect(findTable().attributes('aria-busy')).toBe('false'); + }); + + it('calls graphql api query', () => { + const containerProtectionRuleQueryResolver = jest + .fn() + .mockResolvedValue(containerProtectionRuleQueryPayload()); + createComponent({ containerProtectionRuleQueryResolver }); + + expect(containerProtectionRuleQueryResolver).toHaveBeenCalledWith( + expect.objectContaining({ projectPath: defaultProvidedValues.projectPath }), + ); + }); + + it('shows alert when graphql api query failed', async () => { + const graphqlErrorMessage = 'Error when requesting graphql api'; + const containerProtectionRuleQueryResolver = jest + .fn() + .mockRejectedValue(new Error(graphqlErrorMessage)); + createComponent({ containerProtectionRuleQueryResolver }); + + await waitForPromises(); + + expect(findAlert().isVisible()).toBe(true); + expect(findAlert().text()).toBe(graphqlErrorMessage); + }); + + describe('table pagination', () => { + const findPagination = () => wrapper.findComponent(GlKeysetPagination); + + it('renders pagination', async () => { + createComponent(); + + 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 containerProtectionRuleQueryResolver = jest + .fn() + .mockResolvedValue(containerProtectionRuleQueryPayload()); + createComponent({ containerProtectionRuleQueryResolver }); + + expect(containerProtectionRuleQueryResolver).toHaveBeenCalledWith( + expect.objectContaining({ + projectPath: defaultProvidedValues.projectPath, + first: 10, + }), + ); + }); + + it('show alert when grapqhl fails', () => { + const containerProtectionRuleQueryResolver = jest + .fn() + .mockResolvedValue(containerProtectionRuleQueryPayload()); + createComponent({ containerProtectionRuleQueryResolver }); + + expect(containerProtectionRuleQueryResolver).toHaveBeenCalledWith( + expect.objectContaining({ + projectPath: defaultProvidedValues.projectPath, + first: 10, + }), + ); + }); + + describe('when button "Previous" is clicked', () => { + const containerProtectionRuleQueryResolver = jest + .fn() + .mockResolvedValueOnce( + containerProtectionRuleQueryPayload({ + nodes: containerProtectionRulesData.slice(10), + pageInfo: { + hasNextPage: false, + hasPreviousPage: true, + startCursor: '10', + endCursor: '16', + }, + }), + ) + .mockResolvedValueOnce(containerProtectionRuleQueryPayload()); + + const findPaginationButtonPrev = () => + extendedWrapper(findPagination()).findByRole('button', { name: 'Previous' }); + + beforeEach(async () => { + createComponent({ containerProtectionRuleQueryResolver }); + + await waitForPromises(); + + findPaginationButtonPrev().trigger('click'); + }); + + it('sends a second graphql api query with new pagination params', () => { + expect(containerProtectionRuleQueryResolver).toHaveBeenCalledTimes(2); + expect(containerProtectionRuleQueryResolver).toHaveBeenLastCalledWith( + expect.objectContaining({ + before: '10', + last: 10, + projectPath: 'path', + }), + ); + }); + }); + + describe('when button "Next" is clicked', () => { + const containerProtectionRuleQueryResolver = jest + .fn() + .mockResolvedValueOnce(containerProtectionRuleQueryPayload()) + .mockResolvedValueOnce( + containerProtectionRuleQueryPayload({ + nodes: containerProtectionRulesData.slice(10), + pageInfo: { + hasNextPage: true, + hasPreviousPage: false, + startCursor: '1', + endCursor: '10', + }, + }), + ); + + const findPaginationButtonNext = () => + extendedWrapper(findPagination()).findByRole('button', { name: 'Next' }); + + beforeEach(async () => { + createComponent({ containerProtectionRuleQueryResolver }); + + await waitForPromises(); + + findPaginationButtonNext().trigger('click'); + }); + + it('sends a second graphql api query with new pagination params', () => { + expect(containerProtectionRuleQueryResolver).toHaveBeenCalledTimes(2); + expect(containerProtectionRuleQueryResolver).toHaveBeenLastCalledWith( + expect.objectContaining({ + after: '10', + first: 10, + projectPath: 'path', + }), + ); + }); + }); + }); + }); + + describe('alert "errorMessage"', () => { + const findAlertButtonDismiss = () => wrapper.findByRole('button', { name: /dismiss/i }); + + it('renders alert and dismisses it correctly', async () => { + const alertErrorMessage = 'Error message'; + createComponent({ + config: { + data() { + return { + alertErrorMessage, + }; + }, + }, + }); + + await waitForPromises(); + + expect(findAlert().isVisible()).toBe(true); + expect(findAlert().text()).toBe(alertErrorMessage); + + await findAlertButtonDismiss().trigger('click'); + + expect(findAlert().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js index 1afc9b62ba22a2..cc75e6151ad566 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js @@ -5,6 +5,7 @@ import setWindowLocation from 'helpers/set_window_location_helper'; import * as commonUtils from '~/lib/utils/common_utils'; import component from '~/packages_and_registries/settings/project/components/registry_settings_app.vue'; import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue'; +import ContainerProtectionRules from '~/packages_and_registries/settings/project/components/container_protection_rules.vue'; import PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue'; import PackagesProtectionRules from '~/packages_and_registries/settings/project/components/packages_protection_rules.vue'; import DependencyProxyPackagesSettings from 'ee_component/packages_and_registries/settings/project/components/dependency_proxy_packages_settings.vue'; @@ -19,6 +20,7 @@ describe('Registry Settings app', () => { let wrapper; const findContainerExpirationPolicy = () => wrapper.findComponent(ContainerExpirationPolicy); + const findContainerProtectionRules = () => wrapper.findComponent(ContainerProtectionRules); const findPackagesCleanupPolicy = () => wrapper.findComponent(PackagesCleanupPolicy); const findPackagesProtectionRules = () => wrapper.findComponent(PackagesProtectionRules); const findDependencyProxyPackagesSettings = () => @@ -31,7 +33,10 @@ describe('Registry Settings app', () => { showPackageRegistrySettings: true, showDependencyProxySettings: false, ...(IS_EE && { showDependencyProxySettings: true }), - glFeatures: { packagesProtectedPackages: true }, + glFeatures: { + containerRegistryProtectedContainers: true, + packagesProtectedPackages: true, + }, }; const mountComponent = (provide = defaultProvide) => { @@ -97,6 +102,7 @@ describe('Registry Settings app', () => { }); expect(findContainerExpirationPolicy().exists()).toBe(showContainerRegistrySettings); + expect(findContainerProtectionRules().exists()).toBe(showContainerRegistrySettings); expect(findPackagesCleanupPolicy().exists()).toBe(showPackageRegistrySettings); expect(findPackagesProtectionRules().exists()).toBe(showPackageRegistrySettings); }, @@ -127,5 +133,20 @@ describe('Registry Settings app', () => { }, ); }); + + describe('when feature flag "containerRegistryProtectedContainers" is disabled', () => { + it.each([true, false])( + 'container protection rules settings is hidden if showContainerRegistrySettings is %s', + (showContainerRegistrySettings) => { + mountComponent({ + ...defaultProvide, + showContainerRegistrySettings, + glFeatures: { containerRegistryProtectedContainers: false }, + }); + + expect(findPackagesProtectionRules().exists()).toBe(false); + }, + ); + }); }); }); 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 0b6bdc76f2c428..902dda7835d782 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 @@ -165,3 +165,40 @@ export const updatePackagesProtectionRuleMutationPayload = ({ }, }, }); + +export const containerProtectionRulesData = [ + ...Array.from(Array(15)).map((_e, i) => ({ + id: `gid://gitlab/ContainerRegistry::Protection::Rule/${i}`, + repositoryPathPattern: `@flight/flight/maintainer-${i}-*`, + pushProtectedUpToAccessLevel: 'MAINTAINER', + deleteProtectedUpToAccessLevel: 'MAINTAINER', + })), + { + id: 'gid://gitlab/ContainerRegistry::Protection::Rule/16', + repositoryPathPattern: '@flight/flight/owner-16-*', + pushProtectedUpToAccessLevel: 'OWNER', + deleteProtectedUpToAccessLevel: 'OWNER', + }, +]; + +export const containerProtectionRuleQueryPayload = ({ + errors = [], + nodes = containerProtectionRulesData.slice(0, 10), + pageInfo = { + hasNextPage: true, + hasPreviousPage: false, + startCursor: '0', + endCursor: '10', + }, +} = {}) => ({ + data: { + project: { + id: '1', + containerRegistryProtectionRules: { + nodes, + pageInfo: { __typename: 'PageInfo', ...pageInfo }, + }, + errors, + }, + }, +}); diff --git a/spec/requests/projects/settings/packages_and_registries_controller_spec.rb b/spec/requests/projects/settings/packages_and_registries_controller_spec.rb index c660be0f3bfcc5..aa8d41cf5f8170 100644 --- a/spec/requests/projects/settings/packages_and_registries_controller_spec.rb +++ b/spec/requests/projects/settings/packages_and_registries_controller_spec.rb @@ -43,6 +43,24 @@ expect(response.body).not_to have_pushed_frontend_feature_flags(packagesProtectedPackages: true) end end + + it 'pushes the feature flag "container_registry_protected_containers" to the view' do + subject + + expect(response.body).to have_pushed_frontend_feature_flags(containerRegistryProtectedContainers: true) + end + + context 'when feature flag "container_registry_protected_containers" is disabled' do + before do + stub_feature_flags(container_registry_protected_containers: false) + end + + it 'does not push the feature flag "container_registry_protected_containers" to the view' do + subject + + expect(response.body).not_to have_pushed_frontend_feature_flags(containerRegistryProtectedContainers: true) + end + end end end -- GitLab From 677c100bdf01330558d80435db6f7801ed2a3ce3 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 12 Mar 2024 12:58:34 +0100 Subject: [PATCH 2/3] refactor: Apply suggestion from @mvanremmerden - Adjust wording to stay consistent with the feature "Protected branches" and "Protected tags" - Vertical align columns in the table "Protected containers" --- .../components/container_protection_rules.vue | 16 +++++++++++----- locale/gitlab.pot | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue index 467e1c614789c4..569cff68daf59c 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue @@ -40,7 +40,7 @@ export default { }, inject: ['projectPath'], i18n: { - settingBlockTitle: s__('ContainerRegistry|Container protection rules'), + settingBlockTitle: s__('ContainerRegistry|Protected containers'), settingBlockDescription: s__( 'ContainerRegistry|When a container is protected then only certain user roles are able to update and delete the protected container. This helps to avoid tampering with the container.', ), @@ -94,6 +94,9 @@ export default { isLoadingprotectionRules() { return this.$apollo.queries.protectionRulesQueryPayload.loading; }, + shouldShowPagination() { + return this.protectionRulesQueryPageInfo.hasNextPage; + }, }, methods: { showProtectionRuleForm() { @@ -142,17 +145,17 @@ export default { { key: 'repositoryPathPattern', label: s__('ContainerRegistry|Repository path pattern'), - tdClass: 'gl-w-30', + tdClass: 'gl-w-30 gl-vertical-align-middle!', }, { key: 'pushProtectedUpToAccessLevel', label: I18N_PUSH_PROTECTED_UP_TO_ACCESS_LEVEL, - tdClass: 'gl-w-15', + tdClass: 'gl-w-15 gl-vertical-align-middle!', }, { key: 'deleteProtectedUpToAccessLevel', label: I18N_DELETE_PROTECTED_UP_TO_ACCESS_LEVEL, - tdClass: 'gl-w-15', + tdClass: 'gl-w-15 gl-vertical-align-middle!', }, ], }; @@ -201,7 +204,10 @@ export default { -
+
Date: Mon, 25 Mar 2024 18:27:29 +0000 Subject: [PATCH 3/3] refactor: Apply suggestion from @bauerdominic --- .../project/components/container_protection_rules.vue | 5 ++++- .../settings/project/components/registry_settings_app.vue | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue index 569cff68daf59c..4ffd1e6085c9d0 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue @@ -95,7 +95,10 @@ export default { return this.$apollo.queries.protectionRulesQueryPayload.loading; }, shouldShowPagination() { - return this.protectionRulesQueryPageInfo.hasNextPage; + return ( + this.protectionRulesQueryPageInfo.hasPreviousPage || + this.protectionRulesQueryPageInfo.hasNextPage + ); }, }, methods: { diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue index 2b620ece677545..ffc8d884cb84a2 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue @@ -44,7 +44,7 @@ export default { }, showProtectedContainersSettings() { return ( - this.showContainerRegistrySettings && this.glFeatures.containerRegistryProtectedContainers + this.glFeatures.containerRegistryProtectedContainers && this.showContainerRegistrySettings ); }, }, -- GitLab