diff --git a/app/assets/javascripts/pages/projects/hooks/index.js b/app/assets/javascripts/pages/projects/hooks/index.js index 2a120a690efb25887275c0581e5a801578b1737b..ed476d25f8b6e5c058c1c21486b02d77c49bbacb 100644 --- a/app/assets/javascripts/pages/projects/hooks/index.js +++ b/app/assets/javascripts/pages/projects/hooks/index.js @@ -1,3 +1,5 @@ import initSearchSettings from '~/search_settings'; +import initWebhookForm from '~/webhooks'; initSearchSettings(); +initWebhookForm(); diff --git a/app/assets/javascripts/webhooks/components/form_url_app.vue b/app/assets/javascripts/webhooks/components/form_url_app.vue new file mode 100644 index 0000000000000000000000000000000000000000..62d6c03bbb3b3363a503879c20c3b45de5704378 --- /dev/null +++ b/app/assets/javascripts/webhooks/components/form_url_app.vue @@ -0,0 +1,73 @@ + + + diff --git a/app/assets/javascripts/webhooks/components/form_url_mask_item.vue b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue new file mode 100644 index 0000000000000000000000000000000000000000..1e74b4a8215789c46ca89ba734e5e5f50cf32228 --- /dev/null +++ b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue @@ -0,0 +1,61 @@ + + + diff --git a/app/assets/javascripts/webhooks/index.js b/app/assets/javascripts/webhooks/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bfa33560fa5a6346a762683e97ca185d782ebd7b --- /dev/null +++ b/app/assets/javascripts/webhooks/index.js @@ -0,0 +1,18 @@ +import Vue from 'vue'; +import FormUrlApp from './components/form_url_app.vue'; + +export default () => { + const el = document.querySelector('.js-vue-webhook-form'); + + if (!el) { + return null; + } + + return new Vue({ + el, + name: 'WebhookFormRoot', + render(createElement) { + return createElement(FormUrlApp, {}); + }, + }); +}; diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index afe72767b9a847f1068c18b0e97c85791f6d71d8..549436ccabf04c9d7ef6560edbe57ecd14ba89d0 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -1,10 +1,13 @@ = form_errors(hook) -.form-group - = form.label :url, s_('Webhooks|URL'), class: 'label-bold' - = form.text_field :url, class: 'form-control gl-form-input', placeholder: 'http://example.com/trigger-ci.json' - %p.form-text.text-muted - = s_('Webhooks|URL must be percent-encoded if it contains one or more special characters.') +- if Feature.enabled?(:webhook_form_mask_url) + .js-vue-webhook-form +- else + .form-group + = form.label :url, s_('Webhooks|URL'), class: 'label-bold' + = form.text_field :url, class: 'form-control gl-form-input', placeholder: 'http://example.com/trigger-ci.json' + %p.form-text.text-muted + = s_('Webhooks|URL must be percent-encoded if it contains one or more special characters.') .form-group = form.label :token, s_('Webhooks|Secret token'), class: 'label-bold' = form.text_field :token, class: 'form-control gl-form-input', placeholder: '' diff --git a/config/feature_flags/development/webhook_form_mask_url.yml b/config/feature_flags/development/webhook_form_mask_url.yml new file mode 100644 index 0000000000000000000000000000000000000000..445fcb0b6b3b766d338d7251e79e78bfdfb711a1 --- /dev/null +++ b/config/feature_flags/development/webhook_form_mask_url.yml @@ -0,0 +1,8 @@ +--- +name: webhook_form_mask_url +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/99995 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/376106 +milestone: '15.5' +type: development +group: group::integrations +default_enabled: false diff --git a/ee/app/assets/javascripts/pages/groups/hooks/index.js b/ee/app/assets/javascripts/pages/groups/hooks/index.js index 2a120a690efb25887275c0581e5a801578b1737b..ed476d25f8b6e5c058c1c21486b02d77c49bbacb 100644 --- a/ee/app/assets/javascripts/pages/groups/hooks/index.js +++ b/ee/app/assets/javascripts/pages/groups/hooks/index.js @@ -1,3 +1,5 @@ import initSearchSettings from '~/search_settings'; +import initWebhookForm from '~/webhooks'; initSearchSettings(); +initWebhookForm(); diff --git a/ee/spec/features/groups/hooks/user_adds_hook_spec.rb b/ee/spec/features/groups/hooks/user_adds_hook_spec.rb index 2bad9a8de1aec46197054b279bef96669ab14d48..77e19072368e1a22be75f43fe563ef63b84020e3 100644 --- a/ee/spec/features/groups/hooks/user_adds_hook_spec.rb +++ b/ee/spec/features/groups/hooks/user_adds_hook_spec.rb @@ -15,8 +15,8 @@ visit(group_hooks_path(group)) end - it "adds new hook" do - fill_in("hook_url", with: url) + it "adds new hook", :js do + fill_in("URL", with: url) expect { click_button("Add webhook") }.to change(GroupHook, :count).by(1) expect(page).to have_current_path group_hooks_path(group), ignore_query: true diff --git a/ee/spec/features/groups/hooks/user_edits_hooks_spec.rb b/ee/spec/features/groups/hooks/user_edits_hooks_spec.rb index fc102c2e1faed1e5b6f458d4841c9ae39d5abc7b..63c2db35c5d88e7ca7194039d4e185a89d5ee5a0 100644 --- a/ee/spec/features/groups/hooks/user_edits_hooks_spec.rb +++ b/ee/spec/features/groups/hooks/user_edits_hooks_spec.rb @@ -17,7 +17,7 @@ visit(group_hooks_path(group)) end - it 'updates existing hook' do + it 'updates existing hook', :js do click_link('Edit') expect(page).to have_current_path(edit_group_hook_path(group, hook), ignore_query: true) diff --git a/ee/spec/features/groups/settings/webhooks_settings_spec.rb b/ee/spec/features/groups/settings/webhooks_settings_spec.rb index a629eb0eed0e0c808c2ee8f202ddd943c6ec8289..faee99e65e0a73598a8b398357c8af3a6e8eeded 100644 --- a/ee/spec/features/groups/settings/webhooks_settings_spec.rb +++ b/ee/spec/features/groups/settings/webhooks_settings_spec.rb @@ -69,10 +69,10 @@ expect(page).to have_content('Releases events') end - it 'creates a group hook' do + it 'creates a group hook', :js do visit webhooks_path - fill_in 'hook_url', with: url + fill_in 'URL', with: url check 'Tag push events' fill_in 'hook_push_events_branch_filter', with: 'master' check 'Enable SSL verification' @@ -87,11 +87,11 @@ expect(page).to have_content('Job events') end - it 'edits an existing group hook' do + it 'edits an existing group hook', :js do visit webhooks_path click_link 'Edit' - fill_in 'hook_url', with: url + fill_in 'URL', with: url check 'Enable SSL verification' click_button 'Save changes' diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d912e4536b5b92bdff87be39d1ee03b01cb13c89..d0be3fe73e4015dec3f3f88c43111821e4946dac 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -44984,6 +44984,9 @@ msgstr "" msgid "Webhooks|Deployment events" msgstr "" +msgid "Webhooks|Do not show sensitive data such as tokens in the UI." +msgstr "" + msgid "Webhooks|Enable SSL verification" msgstr "" @@ -44999,12 +45002,18 @@ msgstr "" msgid "Webhooks|Go to webhooks" msgstr "" +msgid "Webhooks|How it looks in the UI" +msgstr "" + msgid "Webhooks|Issues events" msgstr "" msgid "Webhooks|Job events" msgstr "" +msgid "Webhooks|Mask portions of URL" +msgstr "" + msgid "Webhooks|Member events" msgstr "" @@ -45029,6 +45038,12 @@ msgstr "" msgid "Webhooks|Secret token" msgstr "" +msgid "Webhooks|Sensitive portion of URL" +msgstr "" + +msgid "Webhooks|Show full URL" +msgstr "" + msgid "Webhooks|Subgroup events" msgstr "" @@ -45053,6 +45068,9 @@ msgstr "" msgid "Webhooks|URL must be percent-encoded if it contains one or more special characters." msgstr "" +msgid "Webhooks|URL preview" +msgstr "" + msgid "Webhooks|Used to validate received payloads. Sent with the request in the %{code_start}X-Gitlab-Token HTTP%{code_end} header." msgstr "" diff --git a/spec/features/projects/settings/webhooks_settings_spec.rb b/spec/features/projects/settings/webhooks_settings_spec.rb index d525544ac156d1122700f376a96ecb2cda8b5b78..25752bcaf450fc8c1873b66c01be193a5d314d6f 100644 --- a/spec/features/projects/settings/webhooks_settings_spec.rb +++ b/spec/features/projects/settings/webhooks_settings_spec.rb @@ -48,10 +48,10 @@ expect(page).to have_content('Releases events') end - it 'create webhook' do + it 'create webhook', :js do visit webhooks_path - fill_in 'hook_url', with: url + fill_in 'URL', with: url check 'Tag push events' fill_in 'hook_push_events_branch_filter', with: 'master' check 'Enable SSL verification' @@ -66,12 +66,12 @@ expect(page).to have_content('Job events') end - it 'edit existing webhook' do + it 'edit existing webhook', :js do hook visit webhooks_path click_link 'Edit' - fill_in 'hook_url', with: url + fill_in 'URL', with: url check 'Enable SSL verification' click_button 'Save changes' diff --git a/spec/frontend/webhooks/components/form_url_app_spec.js b/spec/frontend/webhooks/components/form_url_app_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..40de3cc0d332610beec9d2c866b4ba8cee613eb1 --- /dev/null +++ b/spec/frontend/webhooks/components/form_url_app_spec.js @@ -0,0 +1,53 @@ +import { nextTick } from 'vue'; +import { GlFormRadio, GlFormRadioGroup } from '@gitlab/ui'; + +import FormUrlApp from '~/webhooks/components/form_url_app.vue'; + +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('FormUrlApp', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(FormUrlApp); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findAllRadioButtons = () => wrapper.findAllComponents(GlFormRadio); + const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup); + const findUrlMaskDisable = () => findAllRadioButtons().at(0); + const findUrlMaskEnable = () => findAllRadioButtons().at(1); + const findUrlMaskSection = () => wrapper.findByTestId('url-mask-section'); + + describe('template', () => { + it('renders radio buttons for URL masking', () => { + createComponent(); + + expect(findAllRadioButtons().length).toBe(2); + expect(findUrlMaskDisable().text()).toBe(FormUrlApp.i18n.radioFullUrlText); + expect(findUrlMaskEnable().text()).toBe(FormUrlApp.i18n.radioMaskUrlText); + }); + + it('does not render mask section', () => { + createComponent(); + + expect(findUrlMaskSection().exists()).toBe(false); + }); + + describe('on radio select', () => { + beforeEach(async () => { + createComponent(); + + findRadioGroup().vm.$emit('input', true); + await nextTick(); + }); + + it('renders mask section', () => { + expect(findUrlMaskSection().exists()).toBe(true); + }); + }); + }); +}); diff --git a/spec/frontend/webhooks/components/form_url_mask_item_spec.js b/spec/frontend/webhooks/components/form_url_mask_item_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..76681e6ab261f9bc4c5ee475668bde7689b2de15 --- /dev/null +++ b/spec/frontend/webhooks/components/form_url_mask_item_spec.js @@ -0,0 +1,51 @@ +import { GlButton, GlFormInput } from '@gitlab/ui'; + +import FormUrlMaskItem from '~/webhooks/components/form_url_mask_item.vue'; + +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('FormUrlMaskItem', () => { + let wrapper; + + const defaultProps = { + index: 0, + }; + + const createComponent = () => { + wrapper = shallowMountExtended(FormUrlMaskItem, { + propsData: { ...defaultProps }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findMaskItemKey = () => wrapper.findByTestId('mask-item-key'); + const findMaskItemValue = () => wrapper.findByTestId('mask-item-value'); + const findRemoveButton = () => wrapper.findComponent(GlButton); + + describe('template', () => { + it('renders input for key and value', () => { + createComponent(); + + const keyInput = findMaskItemKey(); + expect(keyInput.attributes('label')).toBe(FormUrlMaskItem.i18n.keyLabel); + expect(keyInput.findComponent(GlFormInput).attributes('name')).toBe( + 'hook[url_variables][][key]', + ); + + const valueInput = findMaskItemValue(); + expect(valueInput.attributes('label')).toBe(FormUrlMaskItem.i18n.valueLabel); + expect(valueInput.findComponent(GlFormInput).attributes('name')).toBe( + 'hook[url_variables][][value]', + ); + }); + + it('renders remove button', () => { + createComponent(); + + expect(findRemoveButton().props('icon')).toBe('remove'); + }); + }); +});