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 4cc9cc190e813a2e4191601f3093fa0d0c044ea6..06af69ff250e1bdea526242fc5ce3b53ef14df79 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 @@ -6,16 +6,24 @@ import { SHOW_SETUP_SUCCESS_ALERT, UPDATE_SETTINGS_SUCCESS_MESSAGE, } from '~/packages_and_registries/settings/project/constants'; -import ContainerExpirationPolicy from './container_expiration_policy.vue'; -import PackagesCleanupPolicy from './packages_cleanup_policy.vue'; +import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue'; +import PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue'; export default { components: { ContainerExpirationPolicy, + DependencyProxyPackagesSettings: () => + import( + 'ee_component/packages_and_registries/settings/project/components/dependency_proxy_packages_settings.vue' + ), GlAlert, PackagesCleanupPolicy, }, - inject: ['showContainerRegistrySettings', 'showPackageRegistrySettings'], + inject: [ + 'showContainerRegistrySettings', + 'showPackageRegistrySettings', + 'showDependencyProxySettings', + ], i18n: { UPDATE_SETTINGS_SUCCESS_MESSAGE, }, @@ -54,5 +62,6 @@ export default { </gl-alert> <packages-cleanup-policy v-if="showPackageRegistrySettings" /> <container-expiration-policy v-if="showContainerRegistrySettings" /> + <dependency-proxy-packages-settings v-if="showDependencyProxySettings" /> </div> </template> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js index 57c8d07e620418a6bc4900803844b67242e56844..326265430d957913cc9bbb3f9acf67266704c91b 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js +++ b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js @@ -23,6 +23,7 @@ export default () => { helpPagePath, showContainerRegistrySettings, showPackageRegistrySettings, + showDependencyProxySettings, } = el.dataset; return new Vue({ el, @@ -40,6 +41,7 @@ export default () => { helpPagePath, showContainerRegistrySettings: parseBoolean(showContainerRegistrySettings), showPackageRegistrySettings: parseBoolean(showPackageRegistrySettings), + showDependencyProxySettings: parseBoolean(showDependencyProxySettings), }, render(createElement) { return createElement('registry-settings-app', {}); diff --git a/ee/app/assets/javascripts/packages_and_registries/settings/project/components/dependency_proxy_packages_settings.vue b/ee/app/assets/javascripts/packages_and_registries/settings/project/components/dependency_proxy_packages_settings.vue new file mode 100644 index 0000000000000000000000000000000000000000..4d497855bfd6f2da6b52df2025f589a4bdac4add --- /dev/null +++ b/ee/app/assets/javascripts/packages_and_registries/settings/project/components/dependency_proxy_packages_settings.vue @@ -0,0 +1,71 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; +import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue'; +import getDependencyProxyPackagesSettings from 'ee_component/packages_and_registries/settings/project/graphql/queries/get_dependency_proxy_packages_settings.query.graphql'; +import DependencyProxyPackagesSettingsForm from 'ee_component/packages_and_registries/settings/project/components/dependency_proxy_packages_settings_form.vue'; + +export default { + name: 'DependencyProxyPackagesSettings', + components: { + DependencyProxyPackagesSettingsForm, + GlAlert, + SettingsBlock, + }, + inject: { + projectPath: { + default: '', + }, + }, + apollo: { + dependencyProxyPackagesSettings: { + query: getDependencyProxyPackagesSettings, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update: (data) => data.project?.dependencyProxyPackagesSetting || {}, + error(e) { + this.fetchSettingsError = e; + Sentry.captureException(e); + }, + }, + }, + data() { + return { + dependencyProxyPackagesSettings: {}, + fetchSettingsError: false, + }; + }, +}; +</script> + +<template> + <settings-block> + <template #title> + <span data-testid="title">{{ s__('DependencyProxy|Dependency Proxy') }}</span></template + > + <template #description> + <span data-testid="description"> + {{ + s__( + 'DependencyProxy|Enable the Dependency Proxy for packages, and configure connection settings for external registries.', + ) + }} + </span> + </template> + <template #default> + <gl-alert v-if="fetchSettingsError" variant="warning" :dismissible="false"> + {{ + s__('DependencyProxy|Something went wrong while fetching the dependency proxy settings.') + }} + </gl-alert> + <dependency-proxy-packages-settings-form + v-else + v-model="dependencyProxyPackagesSettings" + :is-loading="$apollo.queries.dependencyProxyPackagesSettings.loading" + /> + </template> + </settings-block> +</template> diff --git a/ee/app/assets/javascripts/packages_and_registries/settings/project/components/dependency_proxy_packages_settings_form.vue b/ee/app/assets/javascripts/packages_and_registries/settings/project/components/dependency_proxy_packages_settings_form.vue new file mode 100644 index 0000000000000000000000000000000000000000..6c7d2268afd5b03c571a11c6d4518b42dd005550 --- /dev/null +++ b/ee/app/assets/javascripts/packages_and_registries/settings/project/components/dependency_proxy_packages_settings_form.vue @@ -0,0 +1,65 @@ +<script> +import { GlFormGroup, GlFormInput, GlSkeletonLoader, GlToggle } from '@gitlab/ui'; + +export default { + name: 'DependencyProxyPackagesSettingsForm', + components: { + GlFormGroup, + GlFormInput, + GlSkeletonLoader, + GlToggle, + }, + props: { + value: { + type: Object, + required: true, + }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + mavenExternalRegistryPassword: '', + }; + }, + computed: { + prefilledForm() { + return { + ...this.value, + }; + }, + }, +}; +</script> + +<template> + <gl-skeleton-loader v-if="isLoading" /> + <form v-else> + <gl-toggle + v-model="prefilledForm.enabled" + :label="s__('DependencyProxy|Enable Dependency Proxy')" + /> + <h5 class="gl-mt-6">{{ s__('PackageRegistry|Maven') }}</h5> + <gl-form-group + :label="__('URL')" + :description="s__('DependencyProxy|Base URL of the external registry.')" + > + <gl-form-input v-model="prefilledForm.mavenExternalRegistryUrl" width="xl" /> + </gl-form-group> + <gl-form-group + :label="__('Username')" + :description="s__('DependencyProxy|Username of the external registry.')" + > + <gl-form-input v-model="prefilledForm.mavenExternalRegistryUsername" width="xl" /> + </gl-form-group> + <gl-form-group + :label="__('Password')" + :description="s__('DependencyProxy|Password for your external registry.')" + > + <gl-form-input v-model="mavenExternalRegistryPassword" width="xl" type="password" /> + </gl-form-group> + </form> +</template> diff --git a/ee/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/dependency_proxy_packages_settings.fragment.graphql b/ee/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/dependency_proxy_packages_settings.fragment.graphql new file mode 100644 index 0000000000000000000000000000000000000000..0036587d743dac15c07af2fa34ba6d04efa1def8 --- /dev/null +++ b/ee/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/dependency_proxy_packages_settings.fragment.graphql @@ -0,0 +1,5 @@ +fragment DependencyProxyPackagesSettingFields on DependencyProxyPackagesSetting { + enabled + mavenExternalRegistryUrl + mavenExternalRegistryUsername +} diff --git a/ee/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_dependency_proxy_packages_settings.query.graphql b/ee/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_dependency_proxy_packages_settings.query.graphql new file mode 100644 index 0000000000000000000000000000000000000000..d718805dbe4a95efc1ae1ed0a885f306bc135de4 --- /dev/null +++ b/ee/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_dependency_proxy_packages_settings.query.graphql @@ -0,0 +1,10 @@ +#import "../fragments/dependency_proxy_packages_settings.fragment.graphql" + +query getDependencyProxyPackagesSettings($projectPath: ID!) { + project(fullPath: $projectPath) { + id + dependencyProxyPackagesSetting { + ...DependencyProxyPackagesSettingFields + } + } +} diff --git a/ee/spec/frontend/packages_and_registries/settings/project/components/dependency_proxy_packages_settings_form_spec.js b/ee/spec/frontend/packages_and_registries/settings/project/components/dependency_proxy_packages_settings_form_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..aeef4742bdc2d9d3cbdeb9053110014896619e3a --- /dev/null +++ b/ee/spec/frontend/packages_and_registries/settings/project/components/dependency_proxy_packages_settings_form_spec.js @@ -0,0 +1,100 @@ +import { GlFormGroup, GlFormInput, GlSkeletonLoader, GlToggle } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import DependencyProxyPackagesSettingsForm from 'ee_component/packages_and_registries/settings/project/components/dependency_proxy_packages_settings_form.vue'; +import { dependencyProxyPackagesSettingsPayload } from '../mock_data'; + +describe('Dependency proxy packages settings form', () => { + let wrapper; + + const { + data: { + project: { dependencyProxyPackagesSetting }, + }, + } = dependencyProxyPackagesSettingsPayload(); + + const defaultProps = { + value: { ...dependencyProxyPackagesSetting }, + }; + + const findForm = () => wrapper.find('form'); + const findEnableProxyToggle = () => wrapper.findComponent(GlToggle); + const findLoader = () => wrapper.findComponent(GlSkeletonLoader); + + const mountComponent = ({ props = defaultProps } = {}) => { + wrapper = shallowMountExtended(DependencyProxyPackagesSettingsForm, { + propsData: { ...props }, + }); + }; + + describe('form', () => { + it('is hidden when isLoading is set to true', () => { + mountComponent({ + props: { + ...defaultProps, + isLoading: true, + }, + }); + + expect(findForm().exists()).toBe(false); + expect(findLoader().exists()).toBe(true); + }); + + it('is visible when isLoading is set to false', () => { + mountComponent(); + + expect(findForm().exists()).toBe(true); + expect(findLoader().exists()).toBe(false); + }); + }); + + describe('enable proxy toggle', () => { + it('when enabled', () => { + mountComponent(); + + expect(findEnableProxyToggle().props()).toMatchObject({ + label: 'Enable Dependency Proxy', + value: true, + }); + }); + + it('when disabled', () => { + mountComponent({ + props: { + value: { + ...defaultProps.value, + enabled: false, + }, + }, + }); + + expect(findEnableProxyToggle().props('value')).toBe(false); + }); + }); + + describe('maven registry', () => { + it('renders header', () => { + mountComponent(); + + expect(wrapper.find('h5').text()).toBe('Maven'); + }); + + it.each` + index | field | description | value + ${0} | ${'URL'} | ${'Base URL of the external registry.'} | ${defaultProps.value.mavenExternalRegistryUrl} + ${1} | ${'Username'} | ${'Username of the external registry.'} | ${defaultProps.value.mavenExternalRegistryUsername} + ${2} | ${'Password'} | ${'Password for your external registry.'} | ${''} + `('renders $field', ({ index, field, description, value }) => { + mountComponent(); + + const formGroup = wrapper.findAllComponents(GlFormGroup).at(index); + const formInput = formGroup.findComponent(GlFormInput); + + expect(formGroup.attributes()).toMatchObject({ + label: field, + description, + }); + + expect(formInput.attributes('value')).toBe(value); + }); + }); +}); diff --git a/ee/spec/frontend/packages_and_registries/settings/project/components/dependency_proxy_packages_settings_spec.js b/ee/spec/frontend/packages_and_registries/settings/project/components/dependency_proxy_packages_settings_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..8cc5fdeb75ac64b230cceb617ad31ec8fcdffb2a --- /dev/null +++ b/ee/spec/frontend/packages_and_registries/settings/project/components/dependency_proxy_packages_settings_spec.js @@ -0,0 +1,99 @@ +import { GlAlert } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import DependencyProxyPackagesSettings from 'ee_component/packages_and_registries/settings/project/components/dependency_proxy_packages_settings.vue'; +import DependencyProxyPackagesSettingsForm from 'ee_component/packages_and_registries/settings/project/components/dependency_proxy_packages_settings_form.vue'; +import dependencyProxyPackagesSettingsQuery from 'ee_component/packages_and_registries/settings/project/graphql/queries/get_dependency_proxy_packages_settings.query.graphql'; +import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue'; + +import { + dependencyProxyPackagesSettingsPayload, + dependencyProxyPackagesSettingsData, +} from '../mock_data'; + +Vue.use(VueApollo); + +describe('Dependency proxy packages project settings', () => { + let wrapper; + let fakeApollo; + + const defaultProvidedValues = { + projectPath: 'path', + }; + + const findAlert = () => wrapper.findComponent(GlAlert); + const findFormComponent = () => wrapper.findComponent(DependencyProxyPackagesSettingsForm); + const findTitle = () => wrapper.findByTestId('title'); + const findDescription = () => wrapper.findByTestId('description'); + const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); + + const mountComponent = (provide = defaultProvidedValues, config) => { + wrapper = shallowMountExtended(DependencyProxyPackagesSettings, { + stubs: { + SettingsBlock, + }, + provide, + ...config, + }); + }; + + const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => { + const requestHandlers = [[dependencyProxyPackagesSettingsQuery, resolver]]; + + fakeApollo = createMockApollo(requestHandlers); + mountComponent(provide, { + apolloProvider: fakeApollo, + }); + }; + + afterEach(() => { + fakeApollo = null; + }); + + it('renders settings block component', () => { + mountComponentWithApollo(); + + expect(findSettingsBlock().exists()).toBe(true); + }); + + it('has the correct header text and description', () => { + mountComponentWithApollo(); + + expect(findTitle().text()).toBe('Dependency Proxy'); + expect(findDescription().text()).toBe( + 'Enable the Dependency Proxy for packages, and configure connection settings for external registries.', + ); + }); + + it('renders the setting form', async () => { + mountComponentWithApollo({ + resolver: jest.fn().mockResolvedValue(dependencyProxyPackagesSettingsPayload()), + }); + await waitForPromises(); + + expect(findFormComponent().exists()).toBe(true); + expect(findFormComponent().props('value')).toEqual(dependencyProxyPackagesSettingsData); + }); + + describe('fetchSettingsError', () => { + beforeEach(async () => { + mountComponentWithApollo({ + resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')), + }); + await waitForPromises(); + }); + + it('the form is hidden', () => { + expect(findFormComponent().exists()).toBe(false); + }); + + it('shows an alert', () => { + expect(findAlert().html()).toContain( + 'Something went wrong while fetching the dependency proxy settings.', + ); + }); + }); +}); diff --git a/ee/spec/frontend/packages_and_registries/settings/project/mock_data.js b/ee/spec/frontend/packages_and_registries/settings/project/mock_data.js new file mode 100644 index 0000000000000000000000000000000000000000..42c7cd5c7da645536371f547f936f9d60f26224a --- /dev/null +++ b/ee/spec/frontend/packages_and_registries/settings/project/mock_data.js @@ -0,0 +1,18 @@ +export const dependencyProxyPackagesSettingsData = { + __typename: 'DependencyProxyPackagesSetting', + enabled: true, + mavenExternalRegistryUrl: 'https://test.dev', + mavenExternalRegistryUsername: 'user1', +}; + +export const dependencyProxyPackagesSettingsPayload = (override) => ({ + data: { + project: { + id: '1', + dependencyProxyPackagesSetting: { + ...dependencyProxyPackagesSettingsData, + ...override, + }, + }, + }, +}); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a616fc197dfade2e3bc0af5b607c2fb7dc8f91d0..287df86a49b6d2e8a3d05dbdacb3eac6ef74942b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16559,6 +16559,9 @@ msgstr "" msgid "DependencyProxy|All items in the cache are scheduled for removal." msgstr "" +msgid "DependencyProxy|Base URL of the external registry." +msgstr "" + msgid "DependencyProxy|Cached %{time}" msgstr "" @@ -16595,18 +16598,27 @@ msgstr "" msgid "DependencyProxy|Enable Dependency Proxy" msgstr "" +msgid "DependencyProxy|Enable the Dependency Proxy for packages, and configure connection settings for external registries." +msgstr "" + msgid "DependencyProxy|Enable the Dependency Proxy to cache container images from Docker Hub and automatically clear the cache." msgstr "" msgid "DependencyProxy|Image list" msgstr "" +msgid "DependencyProxy|Password for your external registry." +msgstr "" + msgid "DependencyProxy|Pull image by digest example" msgstr "" msgid "DependencyProxy|Scheduled for deletion" msgstr "" +msgid "DependencyProxy|Something went wrong while fetching the dependency proxy settings." +msgstr "" + msgid "DependencyProxy|There are no images in the cache" msgstr "" @@ -16616,6 +16628,9 @@ msgstr "" msgid "DependencyProxy|To store docker images in Dependency Proxy cache, pull an image by tag in your %{codeStart}.gitlab-ci.yml%{codeEnd} file. In this example, the image is %{codeStart}alpine:latest%{codeEnd}" msgstr "" +msgid "DependencyProxy|Username of the external registry." +msgstr "" + msgid "DependencyProxy|When enabled, images older than 90 days will be removed from the cache." msgstr "" 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 1242590945427a7a229752a6ff2584f910203155..dfcabd14489f1ceb6151cb650bc32ea1343e5b2d 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 @@ -6,6 +6,7 @@ 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 PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue'; +import DependencyProxyPackagesSettings from 'ee_component/packages_and_registries/settings/project/components/dependency_proxy_packages_settings.vue'; import { SHOW_SETUP_SUCCESS_ALERT, UPDATE_SETTINGS_SUCCESS_MESSAGE, @@ -18,11 +19,16 @@ describe('Registry Settings app', () => { const findContainerExpirationPolicy = () => wrapper.findComponent(ContainerExpirationPolicy); const findPackagesCleanupPolicy = () => wrapper.findComponent(PackagesCleanupPolicy); + const findDependencyProxyPackagesSettings = () => + wrapper.findComponent(DependencyProxyPackagesSettings); const findAlert = () => wrapper.findComponent(GlAlert); const defaultProvide = { + projectPath: 'path', showContainerRegistrySettings: true, showPackageRegistrySettings: true, + showDependencyProxySettings: false, + ...(IS_EE && { showDependencyProxySettings: true }), }; const mountComponent = (provide = defaultProvide) => { @@ -82,6 +88,7 @@ describe('Registry Settings app', () => { 'container cleanup policy $showContainerRegistrySettings and package cleanup policy is $showPackageRegistrySettings', ({ showContainerRegistrySettings, showPackageRegistrySettings }) => { mountComponent({ + ...defaultProvide, showContainerRegistrySettings, showPackageRegistrySettings, }); @@ -90,5 +97,16 @@ describe('Registry Settings app', () => { expect(findPackagesCleanupPolicy().exists()).toBe(showPackageRegistrySettings); }, ); + + if (IS_EE) { + it.each([true, false])('when showDependencyProxySettings is %s', (value) => { + mountComponent({ + ...defaultProvide, + showDependencyProxySettings: value, + }); + + expect(findDependencyProxyPackagesSettings().exists()).toBe(value); + }); + } }); });