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 @@
+
+
+
+
+
+
+
+
+
+ {{ $options.i18n.radioFullUrlText }}
+ {{ $options.i18n.radioMaskUrlText }}
+
+ {{ $options.i18n.radioMaskUrlHelp }}
+
+
+
+
+
+
+
+
+
+
+
+
+
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');
+ });
+ });
+});