diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7c27eacd190ef7ddccb9a91281d72cd26f954692
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue
@@ -0,0 +1,177 @@
+<script>
+import { GlAlert, GlButton, GlFormGroup, GlForm, GlFormInput, GlFormSelect } from '@gitlab/ui';
+import createPackagesProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/create_packages_protection_rule.mutation.graphql';
+import { s__, __ } from '~/locale';
+
+const PACKAGES_PROTECTION_RULES_SAVED_SUCCESS_MESSAGE = s__('PackageRegistry|Rule saved.');
+const PACKAGES_PROTECTION_RULES_SAVED_ERROR_MESSAGE = s__(
+  'PackageRegistry|Something went wrong while saving the package protection rule.',
+);
+
+const GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER = 'MAINTAINER';
+const GRAPHQL_ACCESS_LEVEL_VALUE_DEVELOPER = 'DEVELOPER';
+const GRAPHQL_ACCESS_LEVEL_VALUE_OWNER = 'OWNER';
+
+export default {
+  components: {
+    GlButton,
+    GlFormInput,
+    GlFormSelect,
+    GlFormGroup,
+    GlAlert,
+    GlForm,
+  },
+  inject: ['projectPath'],
+  i18n: {
+    PACKAGES_PROTECTION_RULES_SAVED_SUCCESS_MESSAGE,
+    PACKAGES_PROTECTION_RULES_SAVED_ERROR_MESSAGE,
+  },
+  data() {
+    return {
+      packageProtectionRuleFormData: {
+        packageNamePattern: '',
+        packageType: 'NPM',
+        pushProtectedUpToAccessLevel: GRAPHQL_ACCESS_LEVEL_VALUE_DEVELOPER,
+      },
+      updateInProgress: false,
+      alertErrorMessage: '',
+    };
+  },
+  computed: {
+    showLoadingIcon() {
+      return this.updateInProgress;
+    },
+    isEmptyPackageName() {
+      return !this.packageProtectionRuleFormData.packageNamePattern;
+    },
+    isSubmitButtonDisabled() {
+      return this.isEmptyPackageName || this.showLoadingIcon;
+    },
+    isFieldDisabled() {
+      return this.showLoadingIcon;
+    },
+    createPackagesProtectionRuleMutationInput() {
+      return {
+        projectPath: this.projectPath,
+        packageNamePattern: this.packageProtectionRuleFormData.packageNamePattern,
+        packageType: this.packageProtectionRuleFormData.packageType,
+        pushProtectedUpToAccessLevel: this.packageProtectionRuleFormData
+          .pushProtectedUpToAccessLevel,
+      };
+    },
+    packageTypeOptions() {
+      return [{ value: 'NPM', text: s__('PackageRegistry|Npm') }];
+    },
+    pushProtectedUpToAccessLevelOptions() {
+      return [
+        { value: GRAPHQL_ACCESS_LEVEL_VALUE_DEVELOPER, text: __('Developer') },
+        { value: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER, text: __('Maintainer') },
+        { value: GRAPHQL_ACCESS_LEVEL_VALUE_OWNER, text: __('Owner') },
+      ];
+    },
+  },
+  methods: {
+    submit() {
+      this.clearAlertErrorMessage();
+
+      this.updateInProgress = true;
+      return this.$apollo
+        .mutate({
+          mutation: createPackagesProtectionRuleMutation,
+          variables: {
+            input: this.createPackagesProtectionRuleMutationInput,
+          },
+        })
+        .then(({ data }) => {
+          const [errorMessage] = data?.createPackagesProtectionRule?.errors ?? [];
+          if (errorMessage) {
+            this.alertErrorMessage = errorMessage;
+            return;
+          }
+
+          this.$emit('submit', data.createPackagesProtectionRule.packageProtectionRule);
+        })
+        .catch(() => {
+          this.alertErrorMessage = PACKAGES_PROTECTION_RULES_SAVED_ERROR_MESSAGE;
+        })
+        .finally(() => {
+          this.updateInProgress = false;
+        });
+    },
+    clearAlertErrorMessage() {
+      this.alertErrorMessage = null;
+    },
+    cancelForm() {
+      this.clearAlertErrorMessage();
+      this.$emit('cancel');
+    },
+  },
+};
+</script>
+
+<template>
+  <div class="gl-new-card-add-form gl-m-3">
+    <gl-form @submit.prevent="submit" @reset="cancelForm">
+      <gl-alert
+        v-if="alertErrorMessage"
+        class="gl-mb-5"
+        variant="danger"
+        @dismiss="clearAlertErrorMessage"
+      >
+        {{ alertErrorMessage }}
+      </gl-alert>
+
+      <gl-form-group
+        :label="s__('PackageRegistry|Package name pattern')"
+        label-for="input-package-name-pattern"
+      >
+        <gl-form-input
+          id="input-package-name-pattern"
+          v-model.trim="packageProtectionRuleFormData.packageNamePattern"
+          type="text"
+          required
+          :disabled="isFieldDisabled"
+        />
+      </gl-form-group>
+
+      <gl-form-group
+        :label="s__('PackageRegistry|Package type')"
+        label-for="input-package-type"
+        :disabled="isFieldDisabled"
+      >
+        <gl-form-select
+          id="input-package-type"
+          v-model="packageProtectionRuleFormData.packageType"
+          :disabled="isFieldDisabled"
+          :options="packageTypeOptions"
+          required
+        />
+      </gl-form-group>
+
+      <gl-form-group
+        :label="s__('PackageRegistry|Push protected up to access level')"
+        label-for="input-push-protected-up-to-access-level"
+        :disabled="isFieldDisabled"
+      >
+        <gl-form-select
+          id="input-push-protected-up-to-access-level"
+          v-model="packageProtectionRuleFormData.pushProtectedUpToAccessLevel"
+          :options="pushProtectedUpToAccessLevelOptions"
+          :disabled="isFieldDisabled"
+          required
+        />
+      </gl-form-group>
+
+      <div class="gl-display-flex gl-justify-content-start">
+        <gl-button
+          variant="confirm"
+          type="submit"
+          :disabled="isSubmitButtonDisabled"
+          :loading="showLoadingIcon"
+          >{{ __('Protect') }}</gl-button
+        >
+        <gl-button class="gl-ml-3" type="reset">{{ __('Cancel') }}</gl-button>
+      </div>
+    </gl-form>
+  </div>
+</template>
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 d917777880332d37a91b749c0f116b389e2b8d45..3391524a25d9bdc426147d1830a148d978c50cac 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,7 +1,8 @@
 <script>
-import { GlCard, GlTable, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlCard, GlTable, GlLoadingIcon } from '@gitlab/ui';
 import packagesProtectionRuleQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql';
 import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import PackagesProtectionRuleForm from '~/packages_and_registries/settings/project/components/packages_protection_rule_form.vue';
 import { s__ } from '~/locale';
 
 const PAGINATION_DEFAULT_PER_PAGE = 10;
@@ -9,9 +10,11 @@ const PAGINATION_DEFAULT_PER_PAGE = 10;
 export default {
   components: {
     SettingsBlock,
+    GlButton,
     GlCard,
     GlTable,
     GlLoadingIcon,
+    PackagesProtectionRuleForm,
   },
   inject: ['projectPath'],
   i18n: {
@@ -24,6 +27,7 @@ export default {
     return {
       fetchSettingsError: false,
       packageProtectionRules: [],
+      protectionRuleFormVisibility: false,
     };
   },
   computed: {
@@ -43,6 +47,9 @@ export default {
     isLoadingPackageProtectionRules() {
       return this.$apollo.queries.packageProtectionRules.loading;
     },
+    isAddProtectionRuleButtonDisabled() {
+      return this.protectionRuleFormVisibility;
+    },
   },
   apollo: {
     packageProtectionRules: {
@@ -72,6 +79,18 @@ export default {
       label: s__('PackageRegistry|Push protected up to access level'),
     },
   ],
+  methods: {
+    showProtectionRuleForm() {
+      this.protectionRuleFormVisibility = true;
+    },
+    hideProtectionRuleForm() {
+      this.protectionRuleFormVisibility = false;
+    },
+    refetchProtectionRules() {
+      this.$apollo.queries.packageProtectionRules.refetch();
+      this.hideProtectionRuleForm();
+    },
+  },
 };
 </script>
 
@@ -92,16 +111,30 @@ export default {
         <template #header>
           <div class="gl-new-card-title-wrapper gl-justify-content-space-between">
             <h3 class="gl-new-card-title">{{ $options.i18n.settingBlockTitle }}</h3>
+            <div class="gl-new-card-actions">
+              <gl-button
+                size="small"
+                :disabled="isAddProtectionRuleButtonDisabled"
+                @click="showProtectionRuleForm"
+              >
+                {{ s__('PackageRegistry|Add package protection rule') }}
+              </gl-button>
+            </div>
           </div>
         </template>
 
         <template #default>
+          <packages-protection-rule-form
+            v-if="protectionRuleFormVisibility"
+            @cancel="hideProtectionRuleForm"
+            @submit="refetchProtectionRules"
+          />
+
           <gl-table
             :items="tableItems"
             :fields="$options.fields"
             show-empty
             stacked="md"
-            class="mb-3"
             :busy="isLoadingPackageProtectionRules"
           >
             <template #table-busy>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/create_packages_protection_rule.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/create_packages_protection_rule.mutation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..923b9d2e4b26dc16d3ac01c157dadafb2c487ec3
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/create_packages_protection_rule.mutation.graphql
@@ -0,0 +1,11 @@
+mutation createPackagesProtectionRule($input: CreatePackagesProtectionRuleInput!) {
+  createPackagesProtectionRule(input: $input) {
+    packageProtectionRule {
+      id
+      packageNamePattern
+      packageType
+      pushProtectedUpToAccessLevel
+    }
+    errors
+  }
+}
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5ac817e75e050fffc59e630cf977f7877505aa69..053a18852cbac1125b1b5e2451019779c93887f3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -34682,6 +34682,9 @@ msgstr ""
 msgid "PackageRegistry|Add composer registry"
 msgstr ""
 
+msgid "PackageRegistry|Add package protection rule"
+msgstr ""
+
 msgid "PackageRegistry|Additional metadata"
 msgstr ""
 
@@ -34937,6 +34940,9 @@ msgstr ""
 msgid "PackageRegistry|Maven XML"
 msgstr ""
 
+msgid "PackageRegistry|Npm"
+msgstr ""
+
 msgid "PackageRegistry|NuGet"
 msgstr ""
 
@@ -35044,6 +35050,9 @@ msgstr ""
 msgid "PackageRegistry|RubyGems"
 msgstr ""
 
+msgid "PackageRegistry|Rule saved."
+msgstr ""
+
 msgid "PackageRegistry|Show Composer commands"
 msgstr ""
 
@@ -35086,6 +35095,9 @@ msgstr ""
 msgid "PackageRegistry|Something went wrong while fetching the package metadata."
 msgstr ""
 
+msgid "PackageRegistry|Something went wrong while saving the package protection rule."
+msgstr ""
+
 msgid "PackageRegistry|Sorry, your filter produced no results"
 msgstr ""
 
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rule_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rule_form_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..7697b7f6bd789204d7aade9657894252f7d86d21
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rule_form_spec.js
@@ -0,0 +1,227 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { GlForm } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import PackagesProtectionRuleForm from '~/packages_and_registries/settings/project/components/packages_protection_rule_form.vue';
+import createPackagesProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/create_packages_protection_rule.mutation.graphql';
+import {
+  createPackagesProtectionRuleMutationPayload,
+  createPackagesProtectionRuleMutationInput,
+  createPackagesProtectionRuleMutationPayloadErrors,
+} from '../mock_data';
+
+Vue.use(VueApollo);
+
+describe('Packages Protection Rule Form', () => {
+  let wrapper;
+  let fakeApollo;
+
+  const defaultProvidedValues = {
+    projectPath: 'path',
+  };
+
+  const findPackageNamePatternInput = () =>
+    wrapper.findByRole('textbox', { name: /package name pattern/i });
+  const findPackageTypeSelect = () => wrapper.findByRole('combobox', { name: /package type/i });
+  const findPushProtectedUpToAccessLevelSelect = () =>
+    wrapper.findByRole('combobox', { name: /push protected up to access level/i });
+  const findSubmitButton = () => wrapper.findByRole('button', { name: /protect/i });
+  const findForm = () => wrapper.findComponent(GlForm);
+
+  const mountComponent = ({ data, config, provide = defaultProvidedValues } = {}) => {
+    wrapper = mountExtended(PackagesProtectionRuleForm, {
+      provide,
+      data() {
+        return { ...data };
+      },
+      ...config,
+    });
+  };
+
+  const mountComponentWithApollo = ({ provide = defaultProvidedValues, mutationResolver } = {}) => {
+    const requestHandlers = [[createPackagesProtectionRuleMutation, mutationResolver]];
+
+    fakeApollo = createMockApollo(requestHandlers);
+
+    mountComponent({
+      provide,
+      config: {
+        apolloProvider: fakeApollo,
+      },
+    });
+  };
+
+  describe('form fields', () => {
+    describe('form field "packageType"', () => {
+      it('contains only the options for npm', () => {
+        mountComponent();
+
+        expect(findPackageTypeSelect().exists()).toBe(true);
+        const packageTypeSelectOptions = findPackageTypeSelect()
+          .findAll('option')
+          .wrappers.map((option) => option.element.value);
+        expect(packageTypeSelectOptions).toEqual(['NPM']);
+      });
+    });
+
+    describe('form field "pushProtectedUpToAccessLevelSelect"', () => {
+      it('contains only the options for maintainer and owner', () => {
+        mountComponent();
+
+        expect(findPushProtectedUpToAccessLevelSelect().exists()).toBe(true);
+        const pushProtectedUpToAccessLevelSelectOptions = findPushProtectedUpToAccessLevelSelect()
+          .findAll('option')
+          .wrappers.map((option) => option.element.value);
+        expect(pushProtectedUpToAccessLevelSelectOptions).toEqual([
+          'DEVELOPER',
+          'MAINTAINER',
+          'OWNER',
+        ]);
+      });
+    });
+
+    describe('when graphql mutation is in progress', () => {
+      beforeEach(() => {
+        mountComponentWithApollo();
+
+        findForm().trigger('submit');
+      });
+
+      it('disables all form fields', () => {
+        expect(findSubmitButton().props('disabled')).toBe(true);
+        expect(findPackageNamePatternInput().attributes('disabled')).toBe('disabled');
+        expect(findPackageTypeSelect().attributes('disabled')).toBe('disabled');
+        expect(findPushProtectedUpToAccessLevelSelect().attributes('disabled')).toBe('disabled');
+      });
+
+      it('displays a loading spinner', () => {
+        expect(findSubmitButton().props('loading')).toBe(true);
+      });
+    });
+  });
+
+  describe('form actions', () => {
+    describe('button "Protect"', () => {
+      it.each`
+        packageNamePattern                                              | submitButtonDisabled
+        ${''}                                                           | ${true}
+        ${' '}                                                          | ${true}
+        ${createPackagesProtectionRuleMutationInput.packageNamePattern} | ${false}
+      `(
+        'when packageNamePattern is "$packageNamePattern" then the disabled state of the submit button is $submitButtonDisabled',
+        async ({ packageNamePattern, submitButtonDisabled }) => {
+          mountComponent();
+
+          expect(findSubmitButton().props('disabled')).toBe(true);
+
+          await findPackageNamePatternInput().setValue(packageNamePattern);
+
+          expect(findSubmitButton().props('disabled')).toBe(submitButtonDisabled);
+        },
+      );
+    });
+  });
+
+  describe('form events', () => {
+    describe('reset', () => {
+      const mutationResolver = jest
+        .fn()
+        .mockResolvedValue(createPackagesProtectionRuleMutationPayload());
+
+      beforeEach(() => {
+        mountComponentWithApollo({ mutationResolver });
+
+        findForm().trigger('reset');
+      });
+
+      it('emits custom event "cancel"', () => {
+        expect(mutationResolver).not.toHaveBeenCalled();
+
+        expect(wrapper.emitted('cancel')).toBeDefined();
+        expect(wrapper.emitted('cancel')[0]).toEqual([]);
+      });
+
+      it('does not dispatch apollo mutation request', () => {
+        expect(mutationResolver).not.toHaveBeenCalled();
+      });
+
+      it('does not emit custom event "submit"', () => {
+        expect(wrapper.emitted()).not.toHaveProperty('submit');
+      });
+    });
+
+    describe('submit', () => {
+      const findAlert = () => wrapper.findByRole('alert');
+
+      const submitForm = () => {
+        findForm().trigger('submit');
+        return waitForPromises();
+      };
+
+      it('dispatches correct apollo mutation', async () => {
+        const mutationResolver = jest
+          .fn()
+          .mockResolvedValue(createPackagesProtectionRuleMutationPayload());
+
+        mountComponentWithApollo({ mutationResolver });
+
+        await findPackageNamePatternInput().setValue(
+          createPackagesProtectionRuleMutationInput.packageNamePattern,
+        );
+
+        await submitForm();
+
+        expect(mutationResolver).toHaveBeenCalledWith({
+          input: { projectPath: 'path', ...createPackagesProtectionRuleMutationInput },
+        });
+      });
+
+      it('emits event "submit" when apollo mutation successful', async () => {
+        const mutationResolver = jest
+          .fn()
+          .mockResolvedValue(createPackagesProtectionRuleMutationPayload());
+
+        mountComponentWithApollo({ mutationResolver });
+
+        await submitForm();
+
+        expect(wrapper.emitted('submit')).toBeDefined();
+        const expectedEventSubmitPayload = createPackagesProtectionRuleMutationPayload().data
+          .createPackagesProtectionRule.packageProtectionRule;
+        expect(wrapper.emitted('submit')[0]).toEqual([expectedEventSubmitPayload]);
+
+        expect(wrapper.emitted()).not.toHaveProperty('cancel');
+      });
+
+      it('shows error alert with general message when apollo mutation request responds with errors', async () => {
+        mountComponentWithApollo({
+          mutationResolver: jest.fn().mockResolvedValue(
+            createPackagesProtectionRuleMutationPayload({
+              errors: createPackagesProtectionRuleMutationPayloadErrors,
+            }),
+          ),
+        });
+
+        await submitForm();
+
+        expect(findAlert().isVisible()).toBe(true);
+        expect(findAlert().text()).toBe(createPackagesProtectionRuleMutationPayloadErrors[0]);
+      });
+
+      it('shows error alert with general message when apollo mutation request fails', async () => {
+        mountComponentWithApollo({
+          mutationResolver: jest.fn().mockRejectedValue(new Error('GraphQL error')),
+        });
+
+        await submitForm();
+
+        expect(findAlert().isVisible()).toBe(true);
+        expect(findAlert().text()).toMatch(
+          /something went wrong while saving the package protection rule/i,
+        );
+      });
+    });
+  });
+});
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 bdb3db7a1b9b11919341e134079a58aeee232599..26ace764a72fcddc11c81de47086ce2679fa271b 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,11 +1,12 @@
-import { GlTable, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlTable, GlLoadingIcon } from '@gitlab/ui';
 import { shallowMount, mount } from '@vue/test-utils';
 import Vue from 'vue';
 import VueApollo from 'vue-apollo';
 import createMockApollo from 'helpers/mock_apollo_helper';
 import waitForPromises from 'helpers/wait_for_promises';
 import PackagesProtectionRules from '~/packages_and_registries/settings/project/components/packages_protection_rules.vue';
-import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import PackagesProtectionRuleForm from '~/packages_and_registries/settings/project/components/packages_protection_rule_form.vue';
+import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
 import packagesProtectionRuleQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql';
 
 import { packagesProtectionRuleQueryPayload, packagesProtectionRulesData } from '../mock_data';
@@ -22,6 +23,8 @@ describe('Packages protection rules project settings', () => {
   const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
   const findTable = () => wrapper.findComponent(GlTable);
   const findTableLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+  const findProtectionRuleForm = () => wrapper.findComponent(PackagesProtectionRuleForm);
+  const findAddProtectionRuleButton = () => wrapper.findComponent(GlButton);
   const findTableRows = () => findTable().find('tbody').findAll('tr');
 
   const mountComponent = (mountFn = shallowMount, provide = defaultProvidedValues, config) => {
@@ -94,4 +97,73 @@ describe('Packages protection rules project settings', () => {
       expect(findTable().exists()).toBe(true);
     });
   });
+
+  it('does not initially render package protection form', async () => {
+    createComponent();
+
+    await waitForPromises();
+
+    expect(findAddProtectionRuleButton().exists()).toBe(true);
+    expect(findProtectionRuleForm().exists()).toBe(false);
+  });
+
+  describe('button "add protection rule"', () => {
+    it('button exists', async () => {
+      createComponent();
+
+      await waitForPromises();
+
+      expect(findAddProtectionRuleButton().exists()).toBe(true);
+    });
+
+    describe('when button is clicked', () => {
+      beforeEach(async () => {
+        createComponent({ mountFn: mount });
+
+        await waitForPromises();
+
+        await findAddProtectionRuleButton().trigger('click');
+      });
+
+      it('renders package protection form', () => {
+        expect(findProtectionRuleForm().exists()).toBe(true);
+      });
+
+      it('disables the button "add protection rule"', () => {
+        expect(findAddProtectionRuleButton().attributes('disabled')).toBeDefined();
+      });
+    });
+  });
+
+  describe('form "add protection rule"', () => {
+    let resolver;
+
+    beforeEach(async () => {
+      resolver = jest.fn().mockResolvedValue(packagesProtectionRuleQueryPayload());
+
+      createComponent({ resolver, mountFn: mount });
+
+      await waitForPromises();
+
+      await findAddProtectionRuleButton().trigger('click');
+    });
+
+    it("handles event 'submit'", async () => {
+      await findProtectionRuleForm().vm.$emit('submit');
+
+      expect(resolver).toHaveBeenCalledTimes(2);
+
+      expect(findProtectionRuleForm().exists()).toBe(false);
+      expect(findAddProtectionRuleButton().attributes('disabled')).not.toBeDefined();
+    });
+
+    it("handles event 'cancel'", async () => {
+      await findProtectionRuleForm().vm.$emit('cancel');
+
+      expect(resolver).toHaveBeenCalledTimes(1);
+
+      expect(findProtectionRuleForm().exists()).toBe(false);
+      expect(findAddProtectionRuleButton().attributes()).not.toHaveProperty('disabled');
+    });
+  });
 });
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 5c546289b145e22ee4c9dfa504a68b655b0918ed..a8133c0ace6a1a4b1f31edb869d412727d1ceb71 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
@@ -112,3 +112,25 @@ export const packagesProtectionRuleQueryPayload = ({ override, errors = [] } = {
     },
   },
 });
+
+export const createPackagesProtectionRuleMutationPayload = ({ override, errors = [] } = {}) => ({
+  data: {
+    createPackagesProtectionRule: {
+      packageProtectionRule: {
+        ...packagesProtectionRulesData[0],
+        ...override,
+      },
+      errors,
+    },
+  },
+});
+
+export const createPackagesProtectionRuleMutationInput = {
+  packageNamePattern: `@flight/flight-developer-14-*`,
+  packageType: 'NPM',
+  pushProtectedUpToAccessLevel: 'DEVELOPER',
+};
+
+export const createPackagesProtectionRuleMutationPayloadErrors = [
+  'Package name pattern has already been taken',
+];