From 5bd323d4ffb9c2826db588f3473ea0155a19f211 Mon Sep 17 00:00:00 2001 From: Payton Burdette <pburdette@gitlab.com> Date: Wed, 17 Jan 2024 19:38:53 +0000 Subject: [PATCH] Build add pipeline subscription form --- .../pipeline_subscriptions_form.vue | 108 ++++++++++++++++ .../pipeline_subscriptions_table.vue | 21 +++- ...add_pipeline_subscription.mutation.graphql | 8 ++ .../pipeline_subscriptions_app.vue | 6 +- .../pipeline_subscriptions_form_spec.js | 118 ++++++++++++++++++ .../pipeline_subscriptions_table_spec.js | 31 +++++ .../ci/pipeline_subscriptions/mock_data.js | 13 ++ .../pipeline_subscriptions_app_spec.js | 12 ++ locale/gitlab.pot | 9 ++ 9 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 ee/app/assets/javascripts/ci/pipeline_subscriptions/components/pipeline_subscriptions_form.vue create mode 100644 ee/app/assets/javascripts/ci/pipeline_subscriptions/graphql/mutations/add_pipeline_subscription.mutation.graphql create mode 100644 ee/spec/frontend/ci/pipeline_subscriptions/components/pipeline_subscriptions_form_spec.js diff --git a/ee/app/assets/javascripts/ci/pipeline_subscriptions/components/pipeline_subscriptions_form.vue b/ee/app/assets/javascripts/ci/pipeline_subscriptions/components/pipeline_subscriptions_form.vue new file mode 100644 index 0000000000000..36abe0bd98996 --- /dev/null +++ b/ee/app/assets/javascripts/ci/pipeline_subscriptions/components/pipeline_subscriptions_form.vue @@ -0,0 +1,108 @@ +<script> +import { GlButton, GlForm, GlFormGroup, GlFormInput, GlIcon, GlLink } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { __, s__ } from '~/locale'; +import { createAlert } from '~/alert'; +import AddPipelineSubscription from '../graphql/mutations/add_pipeline_subscription.mutation.graphql'; + +export default { + name: 'PipelineSubscriptionsForm', + i18n: { + formLabel: __('Project path'), + inputPlaceholder: __('Paste project path (i.e. gitlab-org/gitlab)'), + subscribe: __('Subscribe'), + cancel: __('Cancel'), + addSubscription: s__('PipelineSubscriptions|Add new pipeline subscription'), + generalError: s__( + 'PipelineSubscriptions|An error occurred while adding a new pipeline subscription.', + ), + addSuccess: s__('PipelineSubscriptions|Subscription successfully added.'), + }, + docsLink: helpPagePath('ci/pipelines/index', { + anchor: 'trigger-a-pipeline-when-an-upstream-project-is-rebuilt', + }), + components: { + GlButton, + GlForm, + GlFormGroup, + GlFormInput, + GlIcon, + GlLink, + }, + inject: { + projectPath: { + default: '', + }, + }, + data() { + return { + upstreamPath: '', + }; + }, + methods: { + async createSubscription() { + try { + const { data } = await this.$apollo.mutate({ + mutation: AddPipelineSubscription, + variables: { + input: { + projectPath: this.projectPath, + upstreamPath: this.upstreamPath, + }, + }, + }); + + if (data.projectSubscriptionCreate.errors.length > 0) { + createAlert({ message: data.projectSubscriptionCreate.errors[0] }); + } else { + createAlert({ message: this.$options.i18n.addSuccess, variant: 'success' }); + this.upstreamPath = ''; + + this.$emit('addSubscriptionSuccess'); + } + } catch (error) { + const { graphQLErrors } = error; + + if (graphQLErrors.length > 0) { + createAlert({ message: graphQLErrors[0]?.message, variant: 'warning' }); + } else { + createAlert({ message: this.$options.i18n.generalError }); + } + } + }, + cancelSubscription() { + this.upstreamPath = ''; + this.$emit('canceled'); + }, + }, +}; +</script> + +<template> + <div class="gl-new-card-add-form gl-m-3"> + <h4 class="gl-mt-0">{{ $options.i18n.addSubscription }}</h4> + <gl-form> + <gl-form-group label-for="project-path"> + <template #label> + {{ $options.i18n.formLabel }} + <gl-link :href="$options.docsLink" target="_blank"> + <gl-icon class="gl-text-blue-600" name="question-o" /> + </gl-link> + </template> + <gl-form-input + id="project-path" + v-model="upstreamPath" + type="text" + :placeholder="$options.i18n.inputPlaceholder" + /> + </gl-form-group> + + <gl-button variant="confirm" data-testid="subscribe-button" @click="createSubscription"> + {{ $options.i18n.subscribe }} + </gl-button> + <gl-button class="gl-ml-3" data-testid="cancel-button" @click="cancelSubscription"> + {{ $options.i18n.cancel }} + </gl-button> + </gl-form> + </div> +</template> diff --git a/ee/app/assets/javascripts/ci/pipeline_subscriptions/components/pipeline_subscriptions_table.vue b/ee/app/assets/javascripts/ci/pipeline_subscriptions/components/pipeline_subscriptions_table.vue index 820a37326150a..135481aec013b 100644 --- a/ee/app/assets/javascripts/ci/pipeline_subscriptions/components/pipeline_subscriptions_table.vue +++ b/ee/app/assets/javascripts/ci/pipeline_subscriptions/components/pipeline_subscriptions_table.vue @@ -1,6 +1,7 @@ <script> import { GlButton, GlCard, GlIcon, GlLink, GlTable, GlTooltipDirective } from '@gitlab/ui'; import { __, s__ } from '~/locale'; +import PipelineSubscriptionsForm from './pipeline_subscriptions_form.vue'; export default { name: 'PipelineSubscriptionsTable', @@ -34,6 +35,7 @@ export default { GlIcon, GlLink, GlTable, + PipelineSubscriptionsForm, }, directives: { GlTooltip: GlTooltipDirective, @@ -61,6 +63,16 @@ export default { required: true, }, }, + data() { + return { + isAddNewClicked: false, + }; + }, + computed: { + showForm() { + return this.showActions && this.isAddNewClicked; + }, + }, }; </script> @@ -81,17 +93,24 @@ export default { </h3> </div> <div v-if="showActions" class="gl-new-card-actions"> - <!-- functionality will be added in https://gitlab.com/gitlab-org/gitlab/-/issues/425293 --> <gl-button + v-if="!isAddNewClicked" size="small" data-testid="add-new-subscription-btn" data-qa-selector="add_new_subscription" + @click="isAddNewClicked = true" > {{ $options.i18n.newBtnText }} </gl-button> </div> </template> + <pipeline-subscriptions-form + v-if="showForm" + @canceled="isAddNewClicked = false" + @addSubscriptionSuccess="$emit('refetchSubscriptions')" + /> + <gl-table :fields="$options.fields" :items="subscriptions" diff --git a/ee/app/assets/javascripts/ci/pipeline_subscriptions/graphql/mutations/add_pipeline_subscription.mutation.graphql b/ee/app/assets/javascripts/ci/pipeline_subscriptions/graphql/mutations/add_pipeline_subscription.mutation.graphql new file mode 100644 index 0000000000000..9637d9c73c83a --- /dev/null +++ b/ee/app/assets/javascripts/ci/pipeline_subscriptions/graphql/mutations/add_pipeline_subscription.mutation.graphql @@ -0,0 +1,8 @@ +mutation addPipelineSubscription($input: ProjectSubscriptionCreateInput!) { + projectSubscriptionCreate(input: $input) { + subscription { + id + } + errors + } +} diff --git a/ee/app/assets/javascripts/ci/pipeline_subscriptions/pipeline_subscriptions_app.vue b/ee/app/assets/javascripts/ci/pipeline_subscriptions/pipeline_subscriptions_app.vue index c235fed1469eb..d00ff50582807 100644 --- a/ee/app/assets/javascripts/ci/pipeline_subscriptions/pipeline_subscriptions_app.vue +++ b/ee/app/assets/javascripts/ci/pipeline_subscriptions/pipeline_subscriptions_app.vue @@ -121,7 +121,7 @@ export default { this.subscriptionToDelete = null; } else { createAlert({ message: this.$options.i18n.deleteSuccess, variant: 'success' }); - this.$apollo.queries.upstreamSubscriptions.refetch(); + this.refetchUpstreamSubscriptions(); } } catch { createAlert({ message: this.$options.i18n.deleteError }); @@ -136,6 +136,9 @@ export default { this.isModalVisible = false; this.subscriptionToDelete = null; }, + refetchUpstreamSubscriptions() { + this.$apollo.queries.upstreamSubscriptions.refetch(); + }, }, }; </script> @@ -151,6 +154,7 @@ export default { :empty-text="$options.i18n.upstreamEmptyText" show-actions @showModal="showModal" + @refetchSubscriptions="refetchUpstreamSubscriptions" /> <gl-loading-icon v-if="downstreamSubscriptionsLoading" /> diff --git a/ee/spec/frontend/ci/pipeline_subscriptions/components/pipeline_subscriptions_form_spec.js b/ee/spec/frontend/ci/pipeline_subscriptions/components/pipeline_subscriptions_form_spec.js new file mode 100644 index 0000000000000..c97bfed7e920e --- /dev/null +++ b/ee/spec/frontend/ci/pipeline_subscriptions/components/pipeline_subscriptions_form_spec.js @@ -0,0 +1,118 @@ +import { GlFormInput, GlLink } from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { createAlert } from '~/alert'; +import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import PipelineSubscriptionsForm from 'ee/ci/pipeline_subscriptions/components/pipeline_subscriptions_form.vue'; +import AddPipelineSubscription from 'ee/ci/pipeline_subscriptions/graphql/mutations/add_pipeline_subscription.mutation.graphql'; + +import { addMutationResponse } from '../mock_data'; + +Vue.use(VueApollo); + +jest.mock('~/alert'); + +describe('Pipeline subscriptions form', () => { + let wrapper; + + const successHandler = jest.fn().mockResolvedValue(addMutationResponse); + const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL Error')); + + const findInput = () => wrapper.findComponent(GlFormInput); + const findHelpLink = () => wrapper.findComponent(GlLink); + const findSubscribeBtn = () => wrapper.findByTestId('subscribe-button'); + const findCancelBtn = () => wrapper.findByTestId('cancel-button'); + + const defaultHandlers = [[AddPipelineSubscription, successHandler]]; + + const defaultProvideOptions = { + projectPath: '/namespace/my-project', + }; + + const upstreamPath = 'root/project'; + + const createMockApolloProvider = (handlers) => { + return createMockApollo(handlers); + }; + + const createComponent = (handlers = defaultHandlers, mountFn = shallowMountExtended) => { + wrapper = mountFn(PipelineSubscriptionsForm, { + provide: { + ...defaultProvideOptions, + }, + apolloProvider: createMockApolloProvider(handlers), + }); + }; + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('displays the upstream input field', () => { + expect(findInput().exists()).toBe(true); + }); + + it('subscribes to an upstream project', async () => { + findInput().vm.$emit('input', upstreamPath); + + findSubscribeBtn().vm.$emit('click'); + + await waitForPromises(); + + expect(successHandler).toHaveBeenCalledWith({ + input: { + projectPath: defaultProvideOptions.projectPath, + upstreamPath, + }, + }); + expect(createAlert).toHaveBeenCalledWith({ + message: 'Subscription successfully added.', + variant: 'success', + }); + }); + + it('cancels adding a subscription and emits the canceled event', async () => { + findInput().vm.$emit('input', upstreamPath); + + await nextTick(); + + expect(findInput().attributes('value')).toBe(upstreamPath); + + findCancelBtn().vm.$emit('click'); + + await nextTick(); + + expect(wrapper.emitted('canceled')).toEqual([[]]); + expect(findInput().attributes('value')).toBe(''); + }); + }); + + describe('errors', () => { + beforeEach(() => { + createComponent([[AddPipelineSubscription, failedHandler]]); + }); + + it('shows alert when error occurs', async () => { + findInput().vm.$emit('input', ''); + + findSubscribeBtn().vm.$emit('click'); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred while adding a new pipeline subscription.', + }); + }); + }); + + it('displays help link to docs', () => { + createComponent(defaultHandlers, mountExtended); + + expect(findHelpLink().attributes('href')).toBe( + '/help/ci/pipelines/index#trigger-a-pipeline-when-an-upstream-project-is-rebuilt', + ); + }); +}); diff --git a/ee/spec/frontend/ci/pipeline_subscriptions/components/pipeline_subscriptions_table_spec.js b/ee/spec/frontend/ci/pipeline_subscriptions/components/pipeline_subscriptions_table_spec.js index 1bd153f37480f..1479c4dc93a28 100644 --- a/ee/spec/frontend/ci/pipeline_subscriptions/components/pipeline_subscriptions_table_spec.js +++ b/ee/spec/frontend/ci/pipeline_subscriptions/components/pipeline_subscriptions_table_spec.js @@ -1,6 +1,8 @@ import { GlTable, GlLink } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import PipelineSubscriptionsTable from 'ee/ci/pipeline_subscriptions/components/pipeline_subscriptions_table.vue'; +import PipelineSubscriptionsForm from 'ee/ci/pipeline_subscriptions/components/pipeline_subscriptions_form.vue'; import { mockUpstreamSubscriptions } from '../mock_data'; describe('Pipeline Subscriptions Table', () => { @@ -30,6 +32,7 @@ describe('Pipeline Subscriptions Table', () => { const findNamespace = () => wrapper.findByTestId('subscription-namespace'); const findProject = () => wrapper.findComponent(GlLink); const findTable = () => wrapper.findComponent(GlTable); + const findForm = () => wrapper.findComponent(PipelineSubscriptionsForm); const createComponent = (props = defaultProps) => { wrapper = mountExtended(PipelineSubscriptionsTable, { @@ -93,4 +96,32 @@ describe('Pipeline Subscriptions Table', () => { expect(findAddNewBtn().exists()).toBe(visible); }, ); + + it('does not display form', () => { + createComponent(); + + expect(findForm().exists()).toBe(false); + }); + + it('displays the form', async () => { + createComponent(); + + findAddNewBtn().vm.$emit('click'); + + await nextTick(); + + expect(findForm().exists()).toBe(true); + }); + + it('hides new button after intial click', async () => { + createComponent(); + + expect(findAddNewBtn().exists()).toBe(true); + + findAddNewBtn().vm.$emit('click'); + + await nextTick(); + + expect(findAddNewBtn().exists()).toBe(false); + }); }); diff --git a/ee/spec/frontend/ci/pipeline_subscriptions/mock_data.js b/ee/spec/frontend/ci/pipeline_subscriptions/mock_data.js index 16ad544304a3d..0e91b61501e96 100644 --- a/ee/spec/frontend/ci/pipeline_subscriptions/mock_data.js +++ b/ee/spec/frontend/ci/pipeline_subscriptions/mock_data.js @@ -15,4 +15,17 @@ export const deleteMutationResponse = { }, }; +export const addMutationResponse = { + data: { + projectSubscriptionCreate: { + subscription: { + id: 'gid://gitlab/Ci::Subscriptions::Project/18', + __typename: 'CiSubscriptionsProject', + }, + errors: [], + __typename: 'ProjectSubscriptionCreatePayload', + }, + }, +}; + export { mockUpstreamSubscriptions, mockDownstreamSubscriptions }; diff --git a/ee/spec/frontend/ci/pipeline_subscriptions/pipeline_subscriptions_app_spec.js b/ee/spec/frontend/ci/pipeline_subscriptions/pipeline_subscriptions_app_spec.js index 9be8190d7680e..71d32900e43eb 100644 --- a/ee/spec/frontend/ci/pipeline_subscriptions/pipeline_subscriptions_app_spec.js +++ b/ee/spec/frontend/ci/pipeline_subscriptions/pipeline_subscriptions_app_spec.js @@ -129,6 +129,18 @@ describe('Pipeline subscriptions app', () => { variant: 'success', }); }); + + it('refetches subscriptions after adding a new subscription', async () => { + createComponent(); + + await waitForPromises(); + + expect(upstreamHanlder).toHaveBeenCalledTimes(1); + + findTables().at(0).vm.$emit('refetchSubscriptions'); + + expect(upstreamHanlder).toHaveBeenCalledTimes(2); + }); }); describe('failures', () => { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e191e928bb0a5..8612b3789790a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -35832,6 +35832,12 @@ msgstr "" msgid "PipelineSubscriptions|Add new" msgstr "" +msgid "PipelineSubscriptions|Add new pipeline subscription" +msgstr "" + +msgid "PipelineSubscriptions|An error occurred while adding a new pipeline subscription." +msgstr "" + msgid "PipelineSubscriptions|An error occurred while deleting this pipeline subscription." msgstr "" @@ -35853,6 +35859,9 @@ msgstr "" msgid "PipelineSubscriptions|Subscription for this project will be removed. Do you want to continue?" msgstr "" +msgid "PipelineSubscriptions|Subscription successfully added." +msgstr "" + msgid "PipelineSubscriptions|Subscription successfully deleted." msgstr "" -- GitLab