diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 0b9e1020bd8916f55767ae44076f29967b6cbdb8..2bfa99fba1cfbddec8adcec721bd7209fa57bb3f 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -42286,6 +42286,7 @@ Types of add-ons. | Value | Description | | ----- | ----------- | | <a id="gitlabsubscriptionsaddontypecode_suggestions"></a>`CODE_SUGGESTIONS` | GitLab Duo Pro seat add-on. | +| <a id="gitlabsubscriptionsaddontypeduo_amazon_q"></a>`DUO_AMAZON_Q` | GitLab Duo with Amazon Q seat add-on. | | <a id="gitlabsubscriptionsaddontypeduo_enterprise"></a>`DUO_ENTERPRISE` | GitLab Duo Enterprise seat add-on. | ### `GitlabSubscriptionsUserRole` diff --git a/ee/app/assets/javascripts/usage_quotas/code_suggestions/components/code_suggestions_info_card.vue b/ee/app/assets/javascripts/usage_quotas/code_suggestions/components/code_suggestions_info_card.vue index a2b342430a4de95dce723a8167c42d37a7cda986..b6420bea587b29532611bf69b4ed24d58b35b2e0 100644 --- a/ee/app/assets/javascripts/usage_quotas/code_suggestions/components/code_suggestions_info_card.vue +++ b/ee/app/assets/javascripts/usage_quotas/code_suggestions/components/code_suggestions_info_card.vue @@ -339,7 +339,7 @@ export default { glm-content="usage-quotas-gitlab-duo-tab" /> <gl-button - v-else + v-else-if="isDuoPro" v-gl-modal-directive="'limited-access-modal-id'" category="primary" target="_blank" diff --git a/ee/app/assets/javascripts/usage_quotas/code_suggestions/components/code_suggestions_usage.vue b/ee/app/assets/javascripts/usage_quotas/code_suggestions/components/code_suggestions_usage.vue index 0e9e0c98e40fb0c42f1f6509a45162c70ba6a5f5..5e716006a2cd848ced7a92c51f6d622db06003a9 100644 --- a/ee/app/assets/javascripts/usage_quotas/code_suggestions/components/code_suggestions_usage.vue +++ b/ee/app/assets/javascripts/usage_quotas/code_suggestions/components/code_suggestions_usage.vue @@ -9,8 +9,9 @@ import getAddOnPurchasesQuery from 'ee/usage_quotas/add_on/graphql/get_add_on_pu import getCurrentLicense from 'ee/admin/subscriptions/show/graphql/queries/get_current_license.query.graphql'; import { - DUO_ENTERPRISE, DUO_PRO, + DUO_ENTERPRISE, + DUO_AMAZON_Q, DUO_TITLES, DUO_BADGE_TITLES, } from 'ee/usage_quotas/code_suggestions/constants'; @@ -172,12 +173,18 @@ export default { return this.queryVariables; }, update({ addOnPurchases }) { - return ( - // Prioritize Duo Enterprise add-on over Duo Pro if both are available to the namespace. - // For example, a namespace can have a Duo Pro add-on but also a Duo Enterprise trial add-on. - addOnPurchases?.find((addOnPurchase) => addOnPurchase.name === DUO_ENTERPRISE) || - addOnPurchases?.find((addOnPurchase) => addOnPurchase.name === DUO_PRO) - ); + // Prioritize Duo with Amazon Q over Duo Enterprise over Duo Pro. + // For example, a namespace can have a Duo Pro add-on but also a Duo Enterprise trial add-on, + // and Duo Enterprise would take precedence + + return addOnPurchases?.reduce((priorityPurchase, currentPurchase) => { + if (currentPurchase.name === DUO_AMAZON_Q) return currentPurchase; + if (priorityPurchase?.name === DUO_AMAZON_Q) return priorityPurchase; + if (currentPurchase.name === DUO_ENTERPRISE) return currentPurchase; + if (priorityPurchase?.name === DUO_ENTERPRISE) return priorityPurchase; + + return currentPurchase; + }, undefined); }, error(error) { const errorWithCause = Object.assign(error, { cause: ADD_ON_PURCHASE_FETCH_ERROR_CODE }); diff --git a/ee/app/assets/javascripts/usage_quotas/code_suggestions/constants.js b/ee/app/assets/javascripts/usage_quotas/code_suggestions/constants.js index fefd38e8646fe8989e4a5cb6ab408fe3cdc89095..7c1c09a32d88aa0c40401373b84932f6820287da 100644 --- a/ee/app/assets/javascripts/usage_quotas/code_suggestions/constants.js +++ b/ee/app/assets/javascripts/usage_quotas/code_suggestions/constants.js @@ -3,28 +3,33 @@ import { __, s__ } from '~/locale'; export const DUO_PRO = 'CODE_SUGGESTIONS'; export const DUO_ENTERPRISE = 'DUO_ENTERPRISE'; -export const DUO_IDENTIFIERS = [DUO_PRO, DUO_ENTERPRISE]; +export const DUO_AMAZON_Q = 'DUO_AMAZON_Q'; +export const DUO_IDENTIFIERS = [DUO_PRO, DUO_ENTERPRISE, DUO_AMAZON_Q]; export const codeSuggestionsLearnMoreLink = `${PROMO_URL}/gitlab-duo/`; export const DUO_TITLES = { [DUO_PRO]: s__('CodeSuggestions|GitLab Duo Pro'), [DUO_ENTERPRISE]: s__('CodeSuggestions|GitLab Duo Enterprise'), + [DUO_AMAZON_Q]: s__('AmazonQ|GitLab Duo with Amazon Q'), }; export const DUO_BADGE_TITLES = { [DUO_PRO]: s__('CodeSuggestions|Pro'), [DUO_ENTERPRISE]: s__('CodeSuggestions|Enterprise'), + [DUO_AMAZON_Q]: __('Amazon Q'), }; export const DUO_ADD_ONS = { [DUO_PRO]: 'codeSuggestionsAddon', [DUO_ENTERPRISE]: 'duoEnterpriseAddon', + [DUO_AMAZON_Q]: 'duoAmazonQAddon', }; export const DUO_CSS_IDENTIFIERS = { [DUO_PRO]: 'duo_pro', [DUO_ENTERPRISE]: 'duo_enterprise', + [DUO_AMAZON_Q]: 'duo_amazon_q', }; export const DUO_HEALTH_CHECK_CATEGORIES = [ @@ -76,6 +81,12 @@ export const addOnEligibleUserListTableFields = { thClass: 'gl-w-5/20', tdClass: '!gl-align-middle', }, + duoAmazonQAddon: { + key: 'codeSuggestionsAddon', + label: DUO_TITLES[DUO_AMAZON_Q], + thClass: 'gl-w-5/20', + tdClass: '!gl-align-middle', + }, email: { key: 'email', label: __('Email'), diff --git a/ee/app/graphql/types/gitlab_subscriptions/add_on_type_enum.rb b/ee/app/graphql/types/gitlab_subscriptions/add_on_type_enum.rb index aa99d6b70b9a9f467a6e5f13131ab0f7bd85b0da..ef14c3b7c45b62776321c6ba5b13e6d3146f1698 100644 --- a/ee/app/graphql/types/gitlab_subscriptions/add_on_type_enum.rb +++ b/ee/app/graphql/types/gitlab_subscriptions/add_on_type_enum.rb @@ -8,6 +8,7 @@ class AddOnTypeEnum < BaseEnum value 'CODE_SUGGESTIONS', value: :code_suggestions, description: 'GitLab Duo Pro seat add-on.' value 'DUO_ENTERPRISE', value: :duo_enterprise, description: 'GitLab Duo Enterprise seat add-on.' + value 'DUO_AMAZON_Q', value: :duo_amazon_q, description: 'GitLab Duo with Amazon Q seat add-on.' end end end diff --git a/ee/spec/frontend/ai/settings/components/duo_seat_utilization_info_card_spec.js b/ee/spec/frontend/ai/settings/components/duo_seat_utilization_info_card_spec.js index 63282f2a7b983423b91ff05f2f969d375235b707..e21f03ec9910fa8b4879659980b98829589ebed2 100644 --- a/ee/spec/frontend/ai/settings/components/duo_seat_utilization_info_card_spec.js +++ b/ee/spec/frontend/ai/settings/components/duo_seat_utilization_info_card_spec.js @@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import { GlCard, GlIcon, GlButton } from '@gitlab/ui'; import UsageStatistics from 'ee/usage_quotas/components/usage_statistics.vue'; import DuoSeatUtilizationInfoCard from 'ee/ai/settings/components/duo_seat_utilization_info_card.vue'; -import { DUO_PRO, DUO_ENTERPRISE, DUO_TITLES } from 'ee/usage_quotas/code_suggestions/constants'; +import { DUO_PRO, DUO_ENTERPRISE, DUO_AMAZON_Q } from 'ee/usage_quotas/code_suggestions/constants'; import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; jest.mock('~/lib/utils/url_utility'); @@ -90,10 +90,13 @@ describe('DuoSeatUtilizationInfoCard', () => { it('sets duoTitle correctly based on duoTier', () => { wrapper = createComponent({ duoTier: DUO_PRO }); - expect(findSeatUtilizationDescription().text()).toContain(DUO_TITLES[DUO_PRO]); + expect(findSeatUtilizationDescription().text()).toContain('GitLab Duo Pro'); wrapper = createComponent({ duoTier: DUO_ENTERPRISE }); - expect(findSeatUtilizationDescription().text()).toContain(DUO_TITLES[DUO_ENTERPRISE]); + expect(findSeatUtilizationDescription().text()).toContain('GitLab Duo Enterprise'); + + wrapper = createComponent({ duoTier: DUO_AMAZON_Q }); + expect(findSeatUtilizationDescription().text()).toContain('GitLab Duo with Amazon Q'); }); it('renders subscription dates correctly', () => { diff --git a/ee/spec/frontend/usage_quotas/code_suggestions/components/add_on_eligible_user_list_spec.js b/ee/spec/frontend/usage_quotas/code_suggestions/components/add_on_eligible_user_list_spec.js index fc83e4a6babb515dc6fdb801f3d52fc86eb1440c..9b141870f49f11098a1e05a48efd28fbb0671a59 100644 --- a/ee/spec/frontend/usage_quotas/code_suggestions/components/add_on_eligible_user_list_spec.js +++ b/ee/spec/frontend/usage_quotas/code_suggestions/components/add_on_eligible_user_list_spec.js @@ -29,7 +29,7 @@ import { import { scrollToElement } from '~/lib/utils/common_utils'; import Tracking from '~/tracking'; import AddOnBulkActionConfirmationModal from 'ee/usage_quotas/code_suggestions/components/add_on_bulk_action_confirmation_modal.vue'; -import { DUO_PRO, DUO_ENTERPRISE, DUO_TITLES } from 'ee/usage_quotas/code_suggestions/constants'; +import { DUO_PRO, DUO_ENTERPRISE, DUO_AMAZON_Q } from 'ee/usage_quotas/code_suggestions/constants'; import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue'; import { createMockClient } from 'helpers/mock_apollo_helper'; import getAddOnEligibleUsers from 'ee/usage_quotas/add_on/graphql/saas_add_on_eligible_users.query.graphql'; @@ -397,7 +397,7 @@ describe('Add On Eligible User List', () => { }); it('labels add-on column as Duo Pro', () => { - expect(findTableLabels()).toContain(DUO_TITLES[DUO_PRO]); + expect(findTableLabels()).toContain('GitLab Duo Pro'); }); describe('with Duo Enterprise add-on enabled', () => { @@ -409,7 +409,20 @@ describe('Add On Eligible User List', () => { }); it('labels add-on column as Duo Enterprise', () => { - expect(findTableLabels()).toContain(DUO_TITLES[DUO_ENTERPRISE]); + expect(findTableLabels()).toContain('GitLab Duo Enterprise'); + }); + }); + + describe('with Duo with Amazon Q add-on enabled', () => { + beforeEach(() => { + return createComponent({ + mountFn: mount, + props: { duoTier: DUO_AMAZON_Q }, + }); + }); + + it('labels add-on column as Duo with Amazon Q', () => { + expect(findTableLabels()).toContain('GitLab Duo with Amazon Q'); }); }); diff --git a/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_addon_assignment_spec.js b/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_addon_assignment_spec.js index e0b3c623cd86f4e9e4ab1be30a1baf5617725ab7..498c4e3cb662b2d0b4c6dd1d452d2a5cfdb0a3b7 100644 --- a/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_addon_assignment_spec.js +++ b/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_addon_assignment_spec.js @@ -2,13 +2,12 @@ import { shallowMount } from '@vue/test-utils'; import { GlToggle } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { sprintf } from '~/locale'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import Tracking from '~/tracking'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import CodeSuggestionsAddonAssignment from 'ee/usage_quotas/code_suggestions/components/code_suggestions_addon_assignment.vue'; -import { DUO_PRO, DUO_ENTERPRISE, DUO_TITLES } from 'ee/usage_quotas/code_suggestions/constants'; +import { DUO_PRO, DUO_ENTERPRISE, DUO_AMAZON_Q } from 'ee/usage_quotas/code_suggestions/constants'; import getAddOnEligibleUsers from 'ee/usage_quotas/add_on/graphql/saas_add_on_eligible_users.query.graphql'; import userAddOnAssignmentCreateMutation from 'ee/usage_quotas/add_on/graphql/user_add_on_assignment_create.mutation.graphql'; import userAddOnAssignmentRemoveMutation from 'ee/usage_quotas/add_on/graphql/user_add_on_assignment_remove.mutation.graphql'; @@ -30,9 +29,11 @@ describe('CodeSuggestionsAddonAssignment', () => { const addOnPurchaseId = 'gid://gitlab/GitlabSubscriptions::AddOnPurchase/2'; const duoEnterpriseAddOnPurchaseId = 'gid://gitlab/GitlabSubscriptions::AddOnPurchase/3'; + const duoAmazonQAddOnPurchaseId = 'gid://gitlab/GitlabSubscriptions::AddOnPurchase/4'; const codeSuggestionsAddOn = { addOnPurchase: { name: DUO_PRO } }; const duoEnterpriseAddOn = { addOnPurchase: { name: DUO_ENTERPRISE } }; + const duoAmazonQAddOn = { addOnPurchase: { name: DUO_AMAZON_Q } }; const addOnPurchase = { id: addOnPurchaseId, @@ -48,6 +49,13 @@ describe('CodeSuggestionsAddonAssignment', () => { assignedQuantity: 2, __typename: 'AddOnPurchase', }; + const duoAmazonQAddOnPurchase = { + id: duoAmazonQAddOnPurchaseId, + name: DUO_AMAZON_Q, + purchasedQuantity: 3, + assignedQuantity: 2, + __typename: 'AddOnPurchase', + }; const addOnEligibleUsersQueryVariables = { fullPath: 'namespace/full-path', @@ -59,6 +67,11 @@ describe('CodeSuggestionsAddonAssignment', () => { addOnType: DUO_ENTERPRISE, addOnPurchaseIds: [duoEnterpriseAddOnPurchaseId], }; + const duoAmazonQAddOnEligibleUsersQueryVariables = { + fullPath: 'namespace/full-path', + addOnType: DUO_AMAZON_Q, + addOnPurchaseIds: [duoAmazonQAddOnPurchaseId], + }; const addOnAssignmentSuccess = { clientMutationId: '1', @@ -86,6 +99,19 @@ describe('CodeSuggestionsAddonAssignment', () => { __typename: 'AddOnUser', }, }; + const duoAmazonQAddOnAssignmentSuccess = { + clientMutationId: '1', + errors: [], + addOnPurchase: duoAmazonQAddOnPurchase, + user: { + id: userIdForAssignment, + addOnAssignments: { + nodes: duoAmazonQAddOn, + __typename: 'UserAddOnAssignmentConnection', + }, + __typename: 'AddOnUser', + }, + }; const addOnUnassignmentSuccess = { clientMutationId: '1', @@ -126,6 +152,9 @@ describe('CodeSuggestionsAddonAssignment', () => { const duoEnterpriseAssignAddOnHandler = jest.fn().mockResolvedValue({ data: { userAddOnAssignmentCreate: duoEnterpriseAddOnAssignmentSuccess }, }); + const duoAmazonQAssignAddOnHandler = jest.fn().mockResolvedValue({ + data: { userAddOnAssignmentCreate: duoAmazonQAddOnAssignmentSuccess }, + }); const unassignAddOnHandler = jest.fn().mockResolvedValue({ data: { userAddOnAssignmentRemove: addOnUnassignmentSuccess }, @@ -188,9 +217,7 @@ describe('CodeSuggestionsAddonAssignment', () => { it('shows correct label on the toggle', () => { createComponent(); - expect(findToggle().props('label')).toBe( - sprintf('%{addOnName} status', { addOnName: DUO_TITLES[DUO_PRO] }), - ); + expect(findToggle().props('label')).toBe('GitLab Duo Pro status'); }); describe('with Duo Enterprise add-on enabled', () => { @@ -199,9 +226,17 @@ describe('CodeSuggestionsAddonAssignment', () => { }); it('shows correct label on the toggle', () => { - expect(findToggle().props('label')).toBe( - sprintf('%{addOnName} status', { addOnName: DUO_TITLES[DUO_ENTERPRISE] }), - ); + expect(findToggle().props('label')).toBe('GitLab Duo Enterprise status'); + }); + }); + + describe('with Duo with Amazon Q add-on enabled', () => { + beforeEach(() => { + return createComponent({ props: { duoTier: DUO_AMAZON_Q } }); + }); + + it('shows correct label on the toggle', () => { + expect(findToggle().props('label')).toBe('GitLab Duo with Amazon Q status'); }); }); @@ -315,6 +350,45 @@ describe('CodeSuggestionsAddonAssignment', () => { }); }); + describe('when assigning a Duo with Amazon Q add-on', () => { + beforeEach(() => { + createComponent({ + props: { + addOnAssignments: [], + duoTier: DUO_AMAZON_Q, + userId: userIdForAssignment, + addOnPurchaseId: duoAmazonQAddOnPurchaseId, + }, + addonAssignmentCreateHandler: duoAmazonQAssignAddOnHandler, + addOnAssignmentQueryVariables: duoAmazonQAddOnEligibleUsersQueryVariables, + }); + + findToggle().vm.$emit('change', true); + }); + + it('updates the cache with latest add-on assignment status', async () => { + await waitForPromises(); + + expect( + getAddOnAssignmentStatusForUserFromCache( + userIdForAssignment, + duoAmazonQAddOnEligibleUsersQueryVariables, + ), + ).toEqual(duoAmazonQAddOn); + }); + + it('calls addon assigment mutation with appropriate params', () => { + expect(duoAmazonQAssignAddOnHandler).toHaveBeenCalledWith({ + addOnPurchaseId: duoAmazonQAddOnPurchaseId, + userId: userIdForAssignment, + }); + }); + + it('does not call addon un-assigment mutation', () => { + expect(unassignAddOnHandler).not.toHaveBeenCalled(); + }); + }); + describe('when error occurs while assigning add-on', () => { const addOnAssignments = []; diff --git a/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_info_card_spec.js b/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_info_card_spec.js index deceea636c9dee212539880f79831c5b96276326..de4a808dacdeef20ab6bf02099ead885cc33d53f 100644 --- a/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_info_card_spec.js +++ b/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_info_card_spec.js @@ -15,7 +15,7 @@ import getGitlabSubscriptionQuery from 'ee/fulfillment/shared_queries/gitlab_sub import { getMockSubscriptionData } from 'ee_jest/usage_quotas/seats/mock_data'; import HandRaiseLeadButton from 'ee/hand_raise_leads/hand_raise_lead/components/hand_raise_lead_button.vue'; import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; -import { DUO_PRO, DUO_ENTERPRISE } from 'ee/usage_quotas/code_suggestions/constants'; +import { DUO_PRO, DUO_ENTERPRISE, DUO_AMAZON_Q } from 'ee/usage_quotas/code_suggestions/constants'; Vue.use(VueApollo); @@ -128,7 +128,6 @@ describe('CodeSuggestionsInfoCard', () => { beforeEach(async () => { createComponent(); - // wait for apollo to load await waitForPromises(); }); @@ -140,7 +139,6 @@ describe('CodeSuggestionsInfoCard', () => { beforeEach(async () => { createComponent({ props: { duoTier: DUO_PRO } }); - // wait for apollo to load await waitForPromises(); }); @@ -164,7 +162,6 @@ describe('CodeSuggestionsInfoCard', () => { beforeEach(async () => { createComponent({ props: { duoTier: DUO_ENTERPRISE } }); - // wait for apollo to load await waitForPromises(); }); @@ -185,6 +182,30 @@ describe('CodeSuggestionsInfoCard', () => { }); }); + describe('with Duo Amazon Q add-on enabled', () => { + beforeEach(async () => { + createComponent({ props: { duoTier: DUO_AMAZON_Q } }); + + await waitForPromises(); + }); + + it('renders the title text', () => { + expect(findCodeSuggestionsInfoTitle().text()).toBe('Subscription'); + }); + + it('tracks the page view correctly', () => { + const { trackEventSpy } = bindInternalEventDocument(wrapper.element); + + expect(trackEventSpy).toHaveBeenCalledWith( + 'view_group_duo_usage_pageload', + { + label: 'duo_amazon_q_add_on_tab', + }, + 'groups:usage_quotas:index', + ); + }); + }); + it('renders the description text', () => { expect(findCodeSuggestionsDescription().text()).toBe( "Code Suggestions uses generative AI to suggest code while you're developing.", @@ -201,7 +222,6 @@ describe('CodeSuggestionsInfoCard', () => { subscriptionData: getMockSubscriptionData({ code: 'premium', name: 'Premium' }), }); - // wait for apollo to load await waitForPromises(); }); it('renders the correct start date text', () => { @@ -216,7 +236,6 @@ describe('CodeSuggestionsInfoCard', () => { beforeEach(async () => { createComponent({ subscriptionData: {} }); - // wait for apollo to load await waitForPromises(); }); it('renders the correct start date text', () => { @@ -234,7 +253,6 @@ describe('CodeSuggestionsInfoCard', () => { provide: { subscriptionStartDate: null, subscriptionEndDate: null }, }); - // wait for apollo to load await waitForPromises(); }); it('renders the correct start date text', () => { @@ -303,7 +321,6 @@ describe('CodeSuggestionsInfoCard', () => { }, }); - // wait for apollo to load await waitForPromises(); }); @@ -438,6 +455,16 @@ describe('CodeSuggestionsInfoCard', () => { describe('when add on is not a trial', () => { describe('when add on is duo enterprise', () => { + it('does not render the purchase seats button', async () => { + createComponent({ + props: { duoTier: DUO_ENTERPRISE }, + provide: { duoAddOnIsTrial: false }, + }); + await waitForPromises(); + + expect(findPurchaseSeatsButton().exists()).toBe(false); + }); + describe('contact sales button', () => { it('is rendered after apollo is loaded with the correct props', async () => { createComponent({ @@ -445,7 +472,6 @@ describe('CodeSuggestionsInfoCard', () => { provide: { duoAddOnIsTrial: false }, }); - // wait for apollo to load await waitForPromises(); expect(findContactSalesButton().exists()).toBe(true); expect(findContactSalesButton().props()).toMatchObject({ @@ -561,11 +587,17 @@ describe('CodeSuggestionsInfoCard', () => { }); describe('when add on is duo pro (code suggestions)', () => { + it('does not render the hand raise lead button', async () => { + createComponent(); + + await waitForPromises(); + expect(findContactSalesButton().exists()).toBe(false); + }); + describe('add seats button', () => { it('is rendered after apollo is loaded', async () => { createComponent(); - // wait for apollo to load await waitForPromises(); expect(findAddSeatsButton().exists()).toBe(true); expect(findAddSeatsButton().text()).toBe('Purchase seats'); @@ -710,6 +742,25 @@ describe('CodeSuggestionsInfoCard', () => { }); }); }); + + describe('when add on is Duo with Amazon Q', () => { + beforeEach(async () => { + createComponent({ + props: { duoTier: DUO_AMAZON_Q }, + provide: { duoAddOnIsTrial: false }, + }); + + await waitForPromises(); + }); + + it('does not render the hand raise lead button', () => { + expect(findContactSalesButton().exists()).toBe(false); + }); + + it('does not render the purchase seats button', () => { + expect(findPurchaseSeatsButton().exists()).toBe(false); + }); + }); }); }); }); diff --git a/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_usage_spec.js b/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_usage_spec.js index 2f508d2c8b2441807889322e88877a0ee156dc21..282f64580709a27f9f425066365c17583bf9edb4 100644 --- a/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_usage_spec.js +++ b/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_usage_spec.js @@ -2,7 +2,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; -import { sprintf } from '~/locale'; import addOnPurchasesQuery from 'ee/usage_quotas/add_on/graphql/get_add_on_purchases.query.graphql'; import getCurrentLicense from 'ee/admin/subscriptions/show/graphql/queries/get_current_license.query.graphql'; import CodeSuggestionsIntro from 'ee/usage_quotas/code_suggestions/components/code_suggestions_intro.vue'; @@ -15,7 +14,7 @@ import { useFakeDate } from 'helpers/fake_date'; import CodeSuggestionsUsageLoader from 'ee/usage_quotas/code_suggestions/components/code_suggestions_usage_loader.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { DUO_TITLES, DUO_PRO, DUO_ENTERPRISE } from 'ee/usage_quotas/code_suggestions/constants'; +import { DUO_PRO, DUO_ENTERPRISE, DUO_AMAZON_Q } from 'ee/usage_quotas/code_suggestions/constants'; import { ADD_ON_ERROR_DICTIONARY, ADD_ON_PURCHASE_FETCH_ERROR_CODE, @@ -29,6 +28,7 @@ import { import { noAssignedDuoProAddonData, noAssignedDuoEnterpriseAddonData, + noAssignedDuoAmazonQAddonData, noAssignedDuoAddonsData, noPurchasedAddonData, purchasedAddonFuzzyData, @@ -51,6 +51,9 @@ describe('GitLab Duo Usage', () => { const noAssignedEnterpriseAddonDataHandler = jest .fn() .mockResolvedValue(noAssignedDuoEnterpriseAddonData); + const noAssignedAmazonQAddonDataHandler = jest + .fn() + .mockResolvedValue(noAssignedDuoAmazonQAddonData); const noAssignedDuoAddonsDataHandler = jest.fn().mockResolvedValue(noAssignedDuoAddonsData); const noPurchasedAddonDataHandler = jest.fn().mockResolvedValue(noPurchasedAddonData); const purchasedAddonFuzzyDataHandler = jest.fn().mockResolvedValue(purchasedAddonFuzzyData); @@ -235,9 +238,7 @@ describe('GitLab Duo Usage', () => { it('renders code suggestions subtitle', () => { expect(findCodeSuggestionsSubtitle().text()).toBe( - sprintf('Manage seat assignments for %{addOnName} within your group.', { - addOnName: DUO_TITLES[DUO_PRO], - }), + 'Manage seat assignments for GitLab Duo Pro within your group.', ); }); @@ -296,7 +297,32 @@ describe('GitLab Duo Usage', () => { }); }); - describe('with both Duo Pro and Enterprise add-ons enabled', () => { + describe('with Duo with Amazon Q add-on enabled', () => { + beforeEach(() => { + return createComponent({ + addOnPurchasesHandler: noAssignedAmazonQAddonDataHandler, + provideProps: { isStandalonePage: true, groupId: 289561 }, + }); + }); + + it('renders code suggestions statistics card for Duo with Amazon Q', () => { + expect(findCodeSuggestionsStatistics().props()).toEqual({ + usageValue: 0, + totalValue: 20, + duoTier: DUO_AMAZON_Q, + }); + }); + + it('renders code suggestions info card for Duo with Amazon Q', () => { + expect(findCodeSuggestionsInfo().exists()).toBe(true); + expect(findCodeSuggestionsInfo().props()).toEqual({ + groupId: 289561, + duoTier: DUO_AMAZON_Q, + }); + }); + }); + + describe('with all Duo add-ons enabled', () => { beforeEach(() => { return createComponent({ addOnPurchasesHandler: noAssignedDuoAddonsDataHandler, @@ -304,26 +330,26 @@ describe('GitLab Duo Usage', () => { }); }); - it('renders addon user list for duo enterprise', () => { + it('renders addon user list for Duo with Amazon Q', () => { expect(findSaasAddOnEligibleUserList().props()).toEqual({ - addOnPurchaseId: 'gid://gitlab/GitlabSubscriptions::AddOnPurchase/4', - duoTier: DUO_ENTERPRISE, + addOnPurchaseId: 'gid://gitlab/GitlabSubscriptions::AddOnPurchase/5', + duoTier: DUO_AMAZON_Q, }); }); - it('renders code suggestions statistics card for duo enterprise', () => { + it('renders code suggestions statistics card for Duo with Amazon Q', () => { expect(findCodeSuggestionsStatistics().props()).toEqual({ usageValue: 0, totalValue: 20, - duoTier: DUO_ENTERPRISE, + duoTier: DUO_AMAZON_Q, }); }); - it('renders code suggestions info card for duo enterprise', () => { + it('renders code suggestions info card for Duo with Amazon Q', () => { expect(findCodeSuggestionsInfo().exists()).toBe(true); expect(findCodeSuggestionsInfo().props()).toEqual({ groupId: 289561, - duoTier: DUO_ENTERPRISE, + duoTier: DUO_AMAZON_Q, }); }); }); @@ -343,9 +369,7 @@ describe('GitLab Duo Usage', () => { it('renders code suggestions subtitle', () => { expect(findCodeSuggestionsSubtitle().text()).toBe( - sprintf('Manage seat assignments for %{addOnName}.', { - addOnName: DUO_TITLES[DUO_PRO], - }), + 'Manage seat assignments for GitLab Duo Pro.', ); }); @@ -357,15 +381,32 @@ describe('GitLab Duo Usage', () => { }); }); - it('renders code suggestions title and enterprise tier badge', () => { + it('renders code suggestions title', () => { + expect(findCodeSuggestionsTitle().text()).toBe('Seat utilization'); + }); + + it('renders code suggestions subtitle', () => { + expect(findCodeSuggestionsSubtitle().text()).toBe( + 'Manage seat assignments for GitLab Duo Enterprise.', + ); + }); + }); + + describe('with Duo Amazon Q add-on enabled', () => { + beforeEach(() => { + return createComponent({ + addOnPurchasesHandler: noAssignedAmazonQAddonDataHandler, + provideProps: { isSaaS: false }, + }); + }); + + it('renders code suggestions title', () => { expect(findCodeSuggestionsTitle().text()).toBe('Seat utilization'); }); it('renders code suggestions subtitle', () => { expect(findCodeSuggestionsSubtitle().text()).toBe( - sprintf('Manage seat assignments for %{addOnName}.', { - addOnName: DUO_TITLES[DUO_ENTERPRISE], - }), + 'Manage seat assignments for GitLab Duo with Amazon Q.', ); }); }); @@ -472,9 +513,7 @@ describe('GitLab Duo Usage', () => { it('renders code suggestions subtitle', () => { expect(findCodeSuggestionsSubtitle().text()).toBe( - sprintf('Manage seat assignments for %{addOnName}.', { - addOnName: DUO_TITLES[DUO_PRO], - }), + 'Manage seat assignments for GitLab Duo Pro.', ); }); diff --git a/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_usage_statistics_card_spec.js b/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_usage_statistics_card_spec.js index 557ea733d2c23911c0fa0bebc7e05e2e096a76bc..c123ef8f3916d1b3593dd62cdca4f30bea60e25f 100644 --- a/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_usage_statistics_card_spec.js +++ b/ee/spec/frontend/usage_quotas/code_suggestions/components/code_suggestions_usage_statistics_card_spec.js @@ -3,7 +3,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import CodeSuggestionsUsageStatisticsCard from 'ee/usage_quotas/code_suggestions/components/code_suggestions_usage_statistics_card.vue'; import UsageStatistics from 'ee/usage_quotas/components/usage_statistics.vue'; -import { DUO_PRO } from 'ee/usage_quotas/code_suggestions/constants'; +import { DUO_PRO, DUO_ENTERPRISE, DUO_AMAZON_Q } from 'ee/usage_quotas/code_suggestions/constants'; describe('CodeSuggestionsUsageStatisticsCard', () => { let wrapper; @@ -57,7 +57,7 @@ describe('CodeSuggestionsUsageStatisticsCard', () => { describe('with purchased Duo Enterprise Add-ons', () => { beforeEach(() => { - return createComponent({ usageValue: 0, totalValue: 20, duoTier: 'DUO_ENTERPRISE' }); + return createComponent({ usageValue: 0, totalValue: 20, duoTier: DUO_ENTERPRISE }); }); it('renders the description text', () => { @@ -79,6 +79,30 @@ describe('CodeSuggestionsUsageStatisticsCard', () => { }); }); + describe('with purchased Duo with Amazon Q Add-ons', () => { + beforeEach(() => { + return createComponent({ usageValue: 0, totalValue: 20, duoTier: DUO_AMAZON_Q }); + }); + + it('renders the description text', () => { + expect(findCodeSuggestionsDescription().text()).toBe( + 'A user can be assigned a GitLab Duo with Amazon Q seat only once each billable month.', + ); + }); + + it('renders the info text', () => { + expect(findCodeSuggestionsInfo().text()).toBe('Seats used'); + }); + + it('passes the correct props to <usage-statistics>', () => { + expect(findUsageStatistics().attributes()).toMatchObject({ + percentage: '0', + 'total-value': '20', + 'usage-value': '0', + }); + }); + }); + describe('with no purchased Add-ons', () => { beforeEach(() => { return createComponent({ usageValue: 0, totalValue: 0 }); diff --git a/ee/spec/frontend/usage_quotas/code_suggestions/components/self_mananged_add_on_eligible_user_list_spec.js b/ee/spec/frontend/usage_quotas/code_suggestions/components/self_mananged_add_on_eligible_user_list_spec.js index b506586d51c2e094d7c0cda5bec50df2d379c47c..2037237a5e91ad42743feb191c414849f73ed8f7 100644 --- a/ee/spec/frontend/usage_quotas/code_suggestions/components/self_mananged_add_on_eligible_user_list_spec.js +++ b/ee/spec/frontend/usage_quotas/code_suggestions/components/self_mananged_add_on_eligible_user_list_spec.js @@ -14,6 +14,7 @@ import { import { DUO_PRO, DUO_ENTERPRISE, + DUO_AMAZON_Q, SORT_OPTIONS, DEFAULT_SORT_OPTION, } from 'ee/usage_quotas/code_suggestions/constants'; @@ -41,6 +42,7 @@ describe('Add On Eligible User List', () => { const addOnPurchaseId = 'gid://gitlab/GitlabSubscriptions::AddOnPurchase/1'; const duoEnterpriseAddOnPurchaseId = 'gid://gitlab/GitlabSubscriptions::AddOnPurchase/2'; + const duoAmazonQAddOnPurchaseId = 'gid://gitlab/GitlabSubscriptions::AddOnPurchase/3'; const error = new Error('Error'); const addOnEligibleUsersResponse = { @@ -72,6 +74,11 @@ describe('Add On Eligible User List', () => { addOnPurchaseIds: [duoEnterpriseAddOnPurchaseId], ...defaultPaginationParams, }; + const defaultDuoAmazonQQueryVariables = { + addOnType: DUO_AMAZON_Q, + addOnPurchaseIds: [duoAmazonQAddOnPurchaseId], + ...defaultPaginationParams, + }; const addOnEligibleUsersDataHandler = jest.fn().mockResolvedValue(addOnEligibleUsersResponse); const addOnEligibleUsersErrorHandler = jest.fn().mockRejectedValue(error); @@ -180,6 +187,18 @@ describe('Add On Eligible User List', () => { }); }); + describe('with Duo with Amazon Q add-on tier', () => { + beforeEach(() => { + return createComponent({ + props: { duoTier: DUO_AMAZON_Q, addOnPurchaseId: duoAmazonQAddOnPurchaseId }, + }); + }); + + it('calls addOnEligibleUsers query with appropriate params', () => { + expect(addOnEligibleUsersDataHandler).toHaveBeenCalledWith(defaultDuoAmazonQQueryVariables); + }); + }); + describe('when there is an error fetching add on eligible users', () => { beforeEach(() => { return createComponent({ handler: addOnEligibleUsersErrorHandler }); diff --git a/ee/spec/frontend/usage_quotas/code_suggestions/mock_data.js b/ee/spec/frontend/usage_quotas/code_suggestions/mock_data.js index b19c5132e92448e49e2b1866390450dd5c69f6f9..952d3d2f9a4c136c2d4a417c6402d56e2e977922 100644 --- a/ee/spec/frontend/usage_quotas/code_suggestions/mock_data.js +++ b/ee/spec/frontend/usage_quotas/code_suggestions/mock_data.js @@ -1,6 +1,6 @@ import { subscriptionTypes } from 'ee/admin/subscriptions/show/constants'; -import { DUO_PRO, DUO_ENTERPRISE } from 'ee/usage_quotas/code_suggestions/constants'; +import { DUO_PRO, DUO_ENTERPRISE, DUO_AMAZON_Q } from 'ee/usage_quotas/code_suggestions/constants'; export const noAssignedDuoProAddonData = { data: { @@ -30,6 +30,20 @@ export const noAssignedDuoEnterpriseAddonData = { }, }; +export const noAssignedDuoAmazonQAddonData = { + data: { + addOnPurchases: [ + { + id: 'gid://gitlab/GitlabSubscriptions::AddOnPurchase/3', + name: DUO_AMAZON_Q, + assignedQuantity: 0, + purchasedQuantity: 20, + __typename: 'AddOnPurchase', + }, + ], + }, +}; + export const noAssignedDuoAddonsData = { data: { addOnPurchases: [ @@ -47,6 +61,27 @@ export const noAssignedDuoAddonsData = { purchasedQuantity: 20, __typename: 'AddOnPurchase', }, + { + id: 'gid://gitlab/GitlabSubscriptions::AddOnPurchase/5', + name: DUO_AMAZON_Q, + assignedQuantity: 0, + purchasedQuantity: 20, + __typename: 'AddOnPurchase', + }, + { + id: 'gid://gitlab/GitlabSubscriptions::AddOnPurchase/6', + name: DUO_PRO, + assignedQuantity: 0, + purchasedQuantity: 15, + __typename: 'AddOnPurchase', + }, + { + id: 'gid://gitlab/GitlabSubscriptions::AddOnPurchase/7', + name: DUO_ENTERPRISE, + assignedQuantity: 0, + purchasedQuantity: 20, + __typename: 'AddOnPurchase', + }, ], }, }; diff --git a/ee/spec/graphql/types/gitlab_subscriptions/add_on_type_enum_spec.rb b/ee/spec/graphql/types/gitlab_subscriptions/add_on_type_enum_spec.rb index 3df913244116bb618db8675a9f9f0334396c7b7a..739b7bbe81f7093aa1d4b8393cdb538e4707b4f6 100644 --- a/ee/spec/graphql/types/gitlab_subscriptions/add_on_type_enum_spec.rb +++ b/ee/spec/graphql/types/gitlab_subscriptions/add_on_type_enum_spec.rb @@ -6,6 +6,6 @@ specify { expect(described_class.graphql_name).to eq('GitlabSubscriptionsAddOnType') } it 'exposes all add-on types' do - expect(described_class.values.keys).to contain_exactly('CODE_SUGGESTIONS', 'DUO_ENTERPRISE') + expect(described_class.values.keys).to contain_exactly('CODE_SUGGESTIONS', 'DUO_ENTERPRISE', 'DUO_AMAZON_Q') end end