From 9b66cd3b2978972d9ba6a7ca8ff29067dac071a4 Mon Sep 17 00:00:00 2001 From: Cindy Halim Date: Thu, 14 Aug 2025 19:34:26 +0800 Subject: [PATCH 1/7] Update core settings visibility for Self-Hosted --- .../components/duo_core_features_form.vue | 43 +++++++++++++------ .../assets/javascripts/ai/settings/index.js | 2 + .../admin/ai_configuration_presenter.rb | 5 +++ .../components/duo_core_features_form_spec.js | 16 +++++++ .../admin/ai_configuration_presenter_spec.rb | 12 ++++++ locale/gitlab.pot | 6 +++ 6 files changed, 72 insertions(+), 12 deletions(-) diff --git a/ee/app/assets/javascripts/ai/settings/components/duo_core_features_form.vue b/ee/app/assets/javascripts/ai/settings/components/duo_core_features_form.vue index 9bea39dc5a6307..b3fa59b7033947 100644 --- a/ee/app/assets/javascripts/ai/settings/components/duo_core_features_form.vue +++ b/ee/app/assets/javascripts/ai/settings/components/duo_core_features_form.vue @@ -15,9 +15,6 @@ export default { name: 'DuoCoreFeaturesForm', i18n: { sectionTitle: __('Gitlab Duo Core'), - subtitle: s__( - 'AiPowered|When turned on, all billable users can access GitLab Duo Chat and Code Suggestions in Web and supported IDEs.', - ), checkboxLabel: s__('AiPowered|Turn on Web and IDE features'), checkboxHelpTextSaaS: s__( 'AiPowered|This setting applies to the whole top-level group. By turning this on, you accept the %{termsLinkStart}GitLab AI Functionality Terms%{termsLinkEnd} unless your organization has a separate agreement with GitLab governing AI feature usage. Check the %{requirementsLinkStart}eligibility requirements%{requirementsLinkEnd}.', @@ -37,7 +34,7 @@ export default { directives: { tooltip: GlTooltipDirective, }, - inject: ['isSaaS'], + inject: ['isSaaS', 'canManageSelfHostedModels', 'hasOnlineLicense'], props: { disabledCheckbox: { type: Boolean, @@ -51,9 +48,35 @@ export default { data() { return { duoCoreEnabled: this.duoCoreFeaturesEnabled, + withSelfHostedModels: this.canManageSelfHostedModels || false, + hasOnlineLicense: this.hasOnlineLicense, }; }, computed: { + checkboxDisabled() { + if (this.withSelfHostedModels && !this.hasOnlineLicense) return true; + + return this.disabledCheckbox; + }, + disabledTooltipText() { + if (this.withSelfHostedModels && !this.hasOnlineLicense) + return s__('AiPowered|This setting requires configuration with GitLab AI Gateway.'); + + return s__( + 'AiPowered|This setting requires GitLab Duo availability to be on or off by default.', + ); + }, + subtitle() { + if (this.withSelfHostedModels && this.hasOnlineLicense) { + return s__( + 'AiPowered|This setting can only currently be used with GitLab AI Gateway and GitLab AI vendor models. AiPowered|When turned on, all billable users can access GitLab Duo Chat and Code Suggestions in Web and supported IDEs.', + ); + } + + return s__( + 'AiPowered|When turned on, all billable users can access GitLab Duo Chat and Code Suggestions in Web and supported IDEs.', + ); + }, description() { return this.isSaaS ? this.$options.i18n.checkboxHelpTextSaaS @@ -73,24 +96,20 @@ export default {
{{ $options.i18n.checkboxLabel }} diff --git a/ee/spec/frontend/ai/settings/components/duo_core_features_form_spec.js b/ee/spec/frontend/ai/settings/components/duo_core_features_form_spec.js index cda24bd48a9f00..daaa5b9aef4d51 100644 --- a/ee/spec/frontend/ai/settings/components/duo_core_features_form_spec.js +++ b/ee/spec/frontend/ai/settings/components/duo_core_features_form_spec.js @@ -1,6 +1,5 @@ import { GlLink, GlSprintf, GlFormGroup, GlFormCheckbox, GlIcon } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import PromoPageLink from '~/vue_shared/components/promo_page_link/promo_page_link.vue'; import DuoCoreFeaturesForm from 'ee/ai/settings/components/duo_core_features_form.vue'; import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility'; @@ -24,9 +23,6 @@ describe('DuoCoreFeaturesForm', () => { hasOnlineLicense: undefined, ...provide, }, - directives: { - tooltip: createMockDirective('gl-tooltip'), - }, stubs: { GlLink, GlSprintf, @@ -39,7 +35,6 @@ describe('DuoCoreFeaturesForm', () => { const findFormCheckbox = () => wrapper.findComponent(GlFormCheckbox); const findButton = () => wrapper.find('button'); const findIcon = () => wrapper.findComponent(GlIcon); - const findTooltip = () => getBinding(findButton().element, 'gl-tooltip'); beforeEach(() => { wrapper = createComponent(); @@ -124,8 +119,9 @@ describe('DuoCoreFeaturesForm', () => { expect(findFormCheckbox().attributes('disabled')).toBeDefined(); }); - it('renders icon and tooltip', () => { - expect(findTooltip().value).toEqual( + it('renders icon and tooltip text', () => { + expect(findIcon().exists()).toBe(true); + expect(findButton().attributes('title')).toEqual( 'This setting requires configuration with GitLab AI Gateway.', ); }); -- GitLab From 9f258d89708f4856b17556a49066f1af9083d0ed Mon Sep 17 00:00:00 2001 From: Cindy Halim Date: Mon, 25 Aug 2025 17:18:59 +0700 Subject: [PATCH 4/7] Implement Duo Self-Hosted warning modal on Duo Core enabled --- .../components/duo_core_features_form.vue | 49 ++++++++----- .../duo_self_hosted_warning_modal.vue | 46 ++++++++++++ .../components/duo_core_features_form_spec.js | 72 +++++++++++++++++-- locale/gitlab.pot | 17 +++-- 4 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 ee/app/assets/javascripts/ai/settings/components/duo_self_hosted_warning_modal.vue diff --git a/ee/app/assets/javascripts/ai/settings/components/duo_core_features_form.vue b/ee/app/assets/javascripts/ai/settings/components/duo_core_features_form.vue index 1d4cdf0fb4811e..cc1e885173b2fb 100644 --- a/ee/app/assets/javascripts/ai/settings/components/duo_core_features_form.vue +++ b/ee/app/assets/javascripts/ai/settings/components/duo_core_features_form.vue @@ -10,11 +10,15 @@ import { import { s__, __ } from '~/locale'; import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility'; import PromoPageLink from '~/vue_shared/components/promo_page_link/promo_page_link.vue'; +import DuoSelfHostedWarningModal from './duo_self_hosted_warning_modal.vue'; export default { name: 'DuoCoreFeaturesForm', i18n: { sectionTitle: __('Gitlab Duo Core'), + subtitle: s__( + 'AiPowered|When turned on, all billable users can access GitLab Duo Chat and Code Suggestions in Web and supported IDEs.', + ), checkboxLabel: s__('AiPowered|Turn on Web and IDE features'), checkboxHelpTextSaaS: s__( 'AiPowered|This setting applies to the whole top-level group. By turning this on, you accept the %{termsLinkStart}GitLab AI Functionality Terms%{termsLinkEnd} unless your organization has a separate agreement with GitLab governing AI feature usage. Check the %{requirementsLinkStart}eligibility requirements%{requirementsLinkEnd}.', @@ -30,6 +34,7 @@ export default { GlLink, GlSprintf, PromoPageLink, + DuoSelfHostedWarningModal, }, directives: { tooltip: GlTooltipDirective, @@ -52,29 +57,18 @@ export default { }, computed: { checkboxDisabled() { - if (this.canManageSelfHostedModels && !this.hasOnlineLicense) return true; + if (this.isDisabledForSelfHostedModels()) return true; return this.disabledCheckbox; }, disabledTooltipText() { - if (this.canManageSelfHostedModels && !this.hasOnlineLicense) - return s__('AiPowered|This setting requires configuration with GitLab AI Gateway.'); + if (this.isDisabledForSelfHostedModels()) + return s__("AiPowered|This setting requires access to GitLab.com's AI Gateway."); return s__( 'AiPowered|This setting requires GitLab Duo availability to be on or off by default.', ); }, - subtitle() { - if (this.canManageSelfHostedModels && this.hasOnlineLicense) { - return s__( - 'AiPowered|This setting can only currently be used with GitLab AI Gateway and GitLab AI vendor models. AiPowered|When turned on, all billable users can access GitLab Duo Chat and Code Suggestions in Web and supported IDEs.', - ); - } - - return s__( - 'AiPowered|When turned on, all billable users can access GitLab Duo Chat and Code Suggestions in Web and supported IDEs.', - ); - }, description() { return this.isSaaS ? this.$options.i18n.checkboxHelpTextSaaS @@ -82,8 +76,24 @@ export default { }, }, methods: { - checkboxChanged() { - this.$emit('change', this.duoCoreEnabled); + isDisabledForSelfHostedModels() { + return Boolean(this.canManageSelfHostedModels && !this.hasOnlineLicense); + }, + toggleDuoCore(value) { + this.$emit('change', value); + this.duoCoreEnabled = value; + }, + checkboxChanged(value) { + if (this.canManageSelfHostedModels && this.hasOnlineLicense && value) { + this.toggleDuoCore(false); + this.$refs.duoSelfHostedWarningModal.show(); + return; + } + + this.toggleDuoCore(value); + }, + enableDuoCore() { + this.toggleDuoCore(true); }, }, requirementsPath: `${DOCS_URL}/subscriptions/subscription-add-ons#gitlab-duo-core`, @@ -94,9 +104,14 @@ export default {
+ +import { GlModal } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + name: 'DuoSelfHostedWarningModal', + components: { + GlModal, + }, + methods: { + // eslint-disable-next-line vue/no-unused-properties -- public API to be used by parent component + show() { + this.$refs.modal.show(); + }, + }, + actionPrimary: { + text: s__('AIPowered|I understand'), + attributes: { + variant: 'confirm', + category: 'primary', + 'data-testid': 'confirm-duo-self-hosted-acknowledgement', + }, + }, +}; + + diff --git a/ee/spec/frontend/ai/settings/components/duo_core_features_form_spec.js b/ee/spec/frontend/ai/settings/components/duo_core_features_form_spec.js index daaa5b9aef4d51..32d4b6fed800d6 100644 --- a/ee/spec/frontend/ai/settings/components/duo_core_features_form_spec.js +++ b/ee/spec/frontend/ai/settings/components/duo_core_features_form_spec.js @@ -1,7 +1,10 @@ +import { nextTick } from 'vue'; import { GlLink, GlSprintf, GlFormGroup, GlFormCheckbox, GlIcon } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { stubComponent } from 'helpers/stub_component'; import PromoPageLink from '~/vue_shared/components/promo_page_link/promo_page_link.vue'; import DuoCoreFeaturesForm from 'ee/ai/settings/components/duo_core_features_form.vue'; +import DuoSelfHostedWarningModal from 'ee/ai/settings/components/duo_self_hosted_warning_modal.vue'; import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility'; const requirementsPath = `${DOCS_URL}/subscriptions/subscription-add-ons#gitlab-duo-core`; @@ -9,6 +12,7 @@ const mockTermsPath = `/handbook/legal/ai-functionality-terms/`; describe('DuoCoreFeaturesForm', () => { let wrapper; + const showModalSpy = jest.fn(); const createComponent = ({ props = {}, provide = {} } = {}) => { return shallowMountExtended(DuoCoreFeaturesForm, { @@ -28,6 +32,11 @@ describe('DuoCoreFeaturesForm', () => { GlSprintf, GlFormGroup, GlFormCheckbox, + DuoSelfHostedWarningModal: stubComponent(DuoSelfHostedWarningModal, { + methods: { + show: showModalSpy, + }, + }), }, }); }; @@ -35,6 +44,7 @@ describe('DuoCoreFeaturesForm', () => { const findFormCheckbox = () => wrapper.findComponent(GlFormCheckbox); const findButton = () => wrapper.find('button'); const findIcon = () => wrapper.findComponent(GlIcon); + const findDuoSelfHostedWarningModal = () => wrapper.findComponent(DuoSelfHostedWarningModal); beforeEach(() => { wrapper = createComponent(); @@ -59,7 +69,7 @@ describe('DuoCoreFeaturesForm', () => { }); it('emits change event when checkbox is clicked', () => { - findFormCheckbox().vm.$emit('change'); + findFormCheckbox().vm.$emit('change', false); expect(wrapper.emitted('change')).toEqual([[false]]); }); @@ -82,6 +92,10 @@ describe('DuoCoreFeaturesForm', () => { it('renders the namespace description', () => { expect(wrapper.text()).toMatch('This setting applies to the whole top-level group.'); }); + + it('does not render Duo Self-Hosted warning modal', () => { + expect(findDuoSelfHostedWarningModal().exists()).toBe(false); + }); }); describe('on Self-Managed', () => { @@ -101,10 +115,52 @@ describe('DuoCoreFeaturesForm', () => { }); }); - it('renders the subtitle', () => { - expect(wrapper.text()).toMatch( - 'This setting can only currently be used with GitLab AI Gateway and GitLab AI vendor models. When turned on, all billable users can access GitLab Duo Chat and Code Suggestions in supported IDEs.', - ); + it('renders Duo Self-Hosted warning modal', () => { + expect(findDuoSelfHostedWarningModal().exists()).toBe(true); + }); + + describe('when enabling Duo Core', () => { + beforeEach(() => { + findFormCheckbox().vm.$emit('change', true); + }); + + it('does not check checkbox initially', () => { + expect(findFormCheckbox().attributes('checked')).toBeUndefined(); + expect(wrapper.emitted('change')).toEqual([[false]]); + }); + + it('shows warning modal', () => { + expect(showModalSpy).toHaveBeenCalledTimes(1); + }); + + it('checks checkbox and emits event if modal primary action button is clicked', async () => { + // Simulate primary action button clicked + findDuoSelfHostedWarningModal().vm.$emit('confirm'); + await nextTick(); + + expect(findFormCheckbox().attributes('checked')).toBe('true'); + expect(wrapper.emitted('change')).toEqual([[false], [true]]); + }); + }); + + describe('when disabling Duo Core', () => { + beforeEach(async () => { + // first, enable Duo Core + findFormCheckbox().vm.$emit('change', true); + findDuoSelfHostedWarningModal().vm.$emit('confirm'); + await nextTick(); + + // disable Duo Core + findFormCheckbox().vm.$emit('change', false); + }); + + it('unchecks checkbox', () => { + expect(findFormCheckbox().attributes('checked')).toBeUndefined(); + }); + + it('does not display warning modal', () => { + expect(showModalSpy).toHaveBeenCalledTimes(1); + }); }); }); @@ -122,9 +178,13 @@ describe('DuoCoreFeaturesForm', () => { it('renders icon and tooltip text', () => { expect(findIcon().exists()).toBe(true); expect(findButton().attributes('title')).toEqual( - 'This setting requires configuration with GitLab AI Gateway.', + "This setting requires access to GitLab.com's AI Gateway.", ); }); + + it('does not render Duo Self-Hosted warning modal', () => { + expect(findDuoSelfHostedWarningModal().exists()).toBe(false); + }); }); it('does not render icon and tooltip initially', () => { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 86d4c36ee1b01f..12dc7ef61c488c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2692,6 +2692,18 @@ msgstr "" msgid "AICatalog|unable to validate definition" msgstr "" +msgid "AIPowered|By enabling Duo Core, you consent to the use of the GitLab AI vendor model, and thus sending data to the GitLab.com AI Gateway, for users that are not assigned Duo seats. If you have Self-Hosted models configured for Code Suggestions and Duo Chat, these models will continue to take precedence for users with assigned Duo seats." +msgstr "" + +msgid "AIPowered|Duo Core grants all billable users access to GitLab Duo Chat and Code Suggestions via Web and supported IDEs. Self-hosted model support is not available at this time." +msgstr "" + +msgid "AIPowered|Duo Core with Duo Self-Hosted" +msgstr "" + +msgid "AIPowered|I understand" +msgstr "" + msgid "AISummary|Generates a summary of this issue" msgstr "" @@ -6406,13 +6418,10 @@ msgstr "" msgid "AiPowered|This setting applies to the whole top-level group. By turning this on, you accept the %{termsLinkStart}GitLab AI Functionality Terms%{termsLinkEnd} unless your organization has a separate agreement with GitLab governing AI feature usage. Check the %{requirementsLinkStart}eligibility requirements%{requirementsLinkEnd}." msgstr "" -msgid "AiPowered|This setting can only currently be used with GitLab AI Gateway and GitLab AI vendor models. When turned on, all billable users can access GitLab Duo Chat and Code Suggestions in supported IDEs." -msgstr "" - msgid "AiPowered|This setting requires GitLab Duo availability to be on or off by default." msgstr "" -msgid "AiPowered|This setting requires configuration with GitLab AI Gateway." +msgid "AiPowered|This setting requires access to GitLab.com's AI Gateway." msgstr "" msgid "AiPowered|Turn off GitLab Duo Agent Platform" -- GitLab From 29cf68a4b2a87a468e96966a5ad3f1cf81a83a70 Mon Sep 17 00:00:00 2001 From: Cindy Halim Date: Tue, 26 Aug 2025 08:33:29 +0700 Subject: [PATCH 5/7] Add modal specs --- .../duo_self_hosted_warning_modal.vue | 3 +-- .../duo_self_hosted_warning_modal_spec.js | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 ee/spec/frontend/ai/settings/components/duo_self_hosted_warning_modal_spec.js diff --git a/ee/app/assets/javascripts/ai/settings/components/duo_self_hosted_warning_modal.vue b/ee/app/assets/javascripts/ai/settings/components/duo_self_hosted_warning_modal.vue index 1fe415cc8b811a..56945d3738cd7a 100644 --- a/ee/app/assets/javascripts/ai/settings/components/duo_self_hosted_warning_modal.vue +++ b/ee/app/assets/javascripts/ai/settings/components/duo_self_hosted_warning_modal.vue @@ -18,7 +18,6 @@ export default { attributes: { variant: 'confirm', category: 'primary', - 'data-testid': 'confirm-duo-self-hosted-acknowledgement', }, }, }; @@ -26,7 +25,7 @@ export default {