From e7fa25217eb9caf60df938875daad6f5adf8eca3 Mon Sep 17 00:00:00 2001 From: Ragnar Date: Tue, 11 Aug 2026 12:13:43 +0200 Subject: [PATCH] feat: Hide n8n-connect usage when aiGatewayCloudUbb is enabled (#35978) --- .../@n8n/api-types/src/frontend-settings.ts | 1 + .../@n8n/backend-common/src/license-state.ts | 4 ++ packages/@n8n/constants/src/index.ts | 1 + .../cli/src/controllers/e2e.controller.ts | 1 + .../__tests__/frontend.service.test.ts | 13 ++++++ packages/cli/src/services/frontend.service.ts | 1 + .../@n8n/stores/src/settings.store.ts | 5 +++ .../src/app/composables/useAiGateway.test.ts | 7 ++- .../app/composables/useSettingsItems.test.ts | 44 +++++++++++++++++++ .../src/app/composables/useSettingsItems.ts | 4 +- .../frontend/editor-ui/src/app/router.test.ts | 9 ++++ packages/frontend/editor-ui/src/app/router.ts | 2 +- 12 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 packages/frontend/editor-ui/src/app/composables/useSettingsItems.test.ts diff --git a/packages/@n8n/api-types/src/frontend-settings.ts b/packages/@n8n/api-types/src/frontend-settings.ts index 600064db942..8b68669f6bc 100644 --- a/packages/@n8n/api-types/src/frontend-settings.ts +++ b/packages/@n8n/api-types/src/frontend-settings.ts @@ -247,6 +247,7 @@ export interface FrontendSettings { aiGateway?: { enabled: boolean; budget: number; + cloudUbbEnabled: boolean; }; ai: { allowSendingParameterValues: boolean; diff --git a/packages/@n8n/backend-common/src/license-state.ts b/packages/@n8n/backend-common/src/license-state.ts index ed00466a570..d9868423884 100644 --- a/packages/@n8n/backend-common/src/license-state.ts +++ b/packages/@n8n/backend-common/src/license-state.ts @@ -114,6 +114,10 @@ export class LicenseState { return this.isLicensed('feat:aiGateway'); } + isAiGatewayCloudUbbLicensed() { + return this.isLicensed('feat:aiGatewayCloudUbb'); + } + isAdvancedExecutionFiltersLicensed() { return this.isLicensed('feat:advancedExecutionFilters'); } diff --git a/packages/@n8n/constants/src/index.ts b/packages/@n8n/constants/src/index.ts index 31072ba6522..52d93e035a2 100644 --- a/packages/@n8n/constants/src/index.ts +++ b/packages/@n8n/constants/src/index.ts @@ -36,6 +36,7 @@ export const LICENSE_FEATURES = { COMMUNITY_NODES_CUSTOM_REGISTRY: 'feat:communityNodes:customRegistry', AI_CREDITS: 'feat:aiCredits', AI_GATEWAY: 'feat:aiGateway', + AI_GATEWAY_CLOUD_UBB: 'feat:aiGatewayCloudUbb', FOLDERS: 'feat:folders', INSIGHTS_VIEW_SUMMARY: 'feat:insights:viewSummary', INSIGHTS_VIEW_DASHBOARD: 'feat:insights:viewDashboard', diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index cf68a585740..67b992d22f1 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -121,6 +121,7 @@ export class E2EController { [LICENSE_FEATURES.ASK_AI]: false, [LICENSE_FEATURES.AI_CREDITS]: false, [LICENSE_FEATURES.AI_GATEWAY]: false, + [LICENSE_FEATURES.AI_GATEWAY_CLOUD_UBB]: false, [LICENSE_FEATURES.FOLDERS]: false, [LICENSE_FEATURES.INSIGHTS_VIEW_SUMMARY]: false, [LICENSE_FEATURES.INSIGHTS_VIEW_DASHBOARD]: false, diff --git a/packages/cli/src/services/__tests__/frontend.service.test.ts b/packages/cli/src/services/__tests__/frontend.service.test.ts index a8e1c37c2b1..a2a8caf61e7 100644 --- a/packages/cli/src/services/__tests__/frontend.service.test.ts +++ b/packages/cli/src/services/__tests__/frontend.service.test.ts @@ -234,6 +234,7 @@ describe('FrontendService', () => { globalConfig.aiAssistant.baseUrl = ''; globalConfig.aiGateway.enabled = false; licenseState.isAiGatewayLicensed.mockReturnValue(false); + licenseState.isAiGatewayCloudUbbLicensed.mockReturnValue(false); }); afterEach(() => { @@ -293,6 +294,18 @@ describe('FrontendService', () => { expect(settings.aiGateway).toBeUndefined(); }); + it('should enable Cloud UBB mode when the AI Gateway is also licensed', async () => { + globalConfig.aiAssistant.baseUrl = 'https://ai-assistant.n8n.io'; + globalConfig.aiGateway.enabled = true; + licenseState.isAiGatewayLicensed.mockReturnValue(true); + licenseState.isAiGatewayCloudUbbLicensed.mockReturnValue(true); + const { service } = createMockService(); + + const settings = await service.getSettings(); + + expect(settings.aiGateway).toMatchObject({ enabled: true, cloudUbbEnabled: true }); + }); + it('should normalize configured postMessage origins', async () => { securityConfig.postMessageAllowedOrigins = 'HTTPS://Example.COM/, https://app.example.com:443, http://localhost:5678/path, not a url, data:text/html;foo'; diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index 06836c8ed4f..04c3490fd2b 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -607,6 +607,7 @@ export class FrontendService { this.settings.aiGateway = { enabled: true, budget: this.license.getValue(LICENSE_QUOTAS.AI_GATEWAY_BUDGET) ?? 0, + cloudUbbEnabled: this.licenseState.isAiGatewayCloudUbbLicensed(), }; } diff --git a/packages/frontend/@n8n/stores/src/settings.store.ts b/packages/frontend/@n8n/stores/src/settings.store.ts index 347976e5073..d29c39195b2 100644 --- a/packages/frontend/@n8n/stores/src/settings.store.ts +++ b/packages/frontend/@n8n/stores/src/settings.store.ts @@ -167,6 +167,10 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { const isAiGatewayEnabled = computed(() => settings.value.aiGateway?.enabled ?? false); + const isAiGatewayCloudUbbEnabled = computed( + () => settings.value.aiGateway?.cloudUbbEnabled ?? false, + ); + const aiGatewayBudget = computed(() => settings.value.aiGateway?.budget ?? 0); const isSmtpSetup = computed(() => userManagement.value.smtpSetup); @@ -498,6 +502,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { aiCreditsQuota, isAiDataSharingEnabled, isAiGatewayEnabled, + isAiGatewayCloudUbbEnabled, aiGatewayBudget, reset, getTimezones, diff --git a/packages/frontend/editor-ui/src/app/composables/useAiGateway.test.ts b/packages/frontend/editor-ui/src/app/composables/useAiGateway.test.ts index 991f2e701c3..4956f03b586 100644 --- a/packages/frontend/editor-ui/src/app/composables/useAiGateway.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useAiGateway.test.ts @@ -30,9 +30,13 @@ vi.mock('@n8n/stores/useRootStore', () => ({ })); const mockIsAiGatewayEnabled = ref(false); +const mockIsAiGatewayCloudUbbEnabled = ref(false); vi.mock('@n8n/stores/settings.store', () => ({ - useSettingsStore: vi.fn(() => ({ isAiGatewayEnabled: mockIsAiGatewayEnabled.value })), + useSettingsStore: vi.fn(() => ({ + isAiGatewayEnabled: mockIsAiGatewayEnabled.value, + isAiGatewayCloudUbbEnabled: mockIsAiGatewayCloudUbbEnabled.value, + })), })); describe('useAiGateway', () => { @@ -40,6 +44,7 @@ describe('useAiGateway', () => { setActivePinia(createPinia()); vi.clearAllMocks(); mockIsAiGatewayEnabled.value = false; + mockIsAiGatewayCloudUbbEnabled.value = false; mockGetGatewayConfig.mockResolvedValue({ nodes: [], credentialTypes: [], providerConfig: {} }); }); diff --git a/packages/frontend/editor-ui/src/app/composables/useSettingsItems.test.ts b/packages/frontend/editor-ui/src/app/composables/useSettingsItems.test.ts new file mode 100644 index 00000000000..b97dac502f7 --- /dev/null +++ b/packages/frontend/editor-ui/src/app/composables/useSettingsItems.test.ts @@ -0,0 +1,44 @@ +import { computed, ref } from 'vue'; + +import { useSettingsItems } from './useSettingsItems'; + +const isAiGatewayCloudUbbEnabled = ref(false); + +vi.mock('vue-router', () => ({ useRouter: vi.fn(() => ({})) })); +vi.mock('./useUserHelpers', () => ({ + useUserHelpers: vi.fn(() => ({ canUserAccessRouteByName: vi.fn(() => true) })), +})); +vi.mock('./useAiGateway', () => ({ + useAiGateway: vi.fn(() => ({ balance: computed(() => undefined) })), +})); +vi.mock('@n8n/i18n', () => ({ useI18n: vi.fn(() => ({ baseText: (key: string) => key })) })); +vi.mock('../stores/ui.store', () => ({ useUIStore: vi.fn(() => ({ settingsSidebarItems: [] })) })); +vi.mock('@n8n/stores/settings.store', () => ({ + useSettingsStore: vi.fn(() => ({ + isAiAssistantEnabled: false, + isAiGatewayEnabled: true, + isAiGatewayCloudUbbEnabled: isAiGatewayCloudUbbEnabled.value, + isPublicApiEnabled: false, + isQueueModeEnabled: false, + isModuleActive: vi.fn(() => false), + })), +})); +vi.mock('../utils/rbac/permissions', () => ({ hasPermission: vi.fn(() => false) })); +vi.mock('@/features/shared/envFeatureFlag/useEnvFeatureFlag', () => ({ + useEnvFeatureFlag: vi.fn(() => ({ check: computed(() => () => false) })), +})); + +describe('useSettingsItems', () => { + it.each([ + [false, true], + [true, false], + ])('shows n8n Connect only when Cloud UBB is %s', (cloudUbbEnabled, visible) => { + isAiGatewayCloudUbbEnabled.value = cloudUbbEnabled; + + const item = useSettingsItems().settingsItems.value.find( + ({ id }) => id === 'settings-n8n-connect', + ); + + expect(item !== undefined).toBe(visible); + }); +}); diff --git a/packages/frontend/editor-ui/src/app/composables/useSettingsItems.ts b/packages/frontend/editor-ui/src/app/composables/useSettingsItems.ts index b6f0520eb7b..f7f9ec20d12 100644 --- a/packages/frontend/editor-ui/src/app/composables/useSettingsItems.ts +++ b/packages/frontend/editor-ui/src/app/composables/useSettingsItems.ts @@ -61,7 +61,9 @@ export function useSettingsItems() { label: i18n.baseText('settings.n8nConnect'), position: 'top', available: - settingsStore.isAiGatewayEnabled && canUserAccessRouteByName(VIEWS.AI_GATEWAY_SETTINGS), + settingsStore.isAiGatewayEnabled && + !settingsStore.isAiGatewayCloudUbbEnabled && + canUserAccessRouteByName(VIEWS.AI_GATEWAY_SETTINGS), route: { to: { name: VIEWS.AI_GATEWAY_SETTINGS } }, creditsTag: balance.value !== undefined diff --git a/packages/frontend/editor-ui/src/app/router.test.ts b/packages/frontend/editor-ui/src/app/router.test.ts index a19dc4ebb90..91b5cb8713d 100644 --- a/packages/frontend/editor-ui/src/app/router.test.ts +++ b/packages/frontend/editor-ui/src/app/router.test.ts @@ -37,6 +37,7 @@ describe('router', () => { beforeEach(async () => { settingsStore = useSettingsStore(); + settingsStore.settings.aiGateway = undefined; const usersStore = useUsersStore(); initializeAuthenticatedFeaturesSpy = vi .spyOn(init, 'initializeAuthenticatedFeatures') @@ -201,6 +202,14 @@ describe('router', () => { expect(router.currentRoute.value.name).toBe(name); }); + test('should block n8n Connect settings for Cloud UBB', async () => { + settingsStore.settings.aiGateway = { enabled: true, budget: 0, cloudUbbEnabled: true }; + + await router.push('/settings/n8n-connect'); + + expect(router.currentRoute.value.name).toBe(VIEWS.WORKFLOWS); + }); + describe('resource center route guard', () => { beforeEach(async () => { // Reset to a neutral route so each test's push('/resource-center') diff --git a/packages/frontend/editor-ui/src/app/router.ts b/packages/frontend/editor-ui/src/app/router.ts index e577fa3849d..26ee37f9f88 100644 --- a/packages/frontend/editor-ui/src/app/router.ts +++ b/packages/frontend/editor-ui/src/app/router.ts @@ -768,7 +768,7 @@ export const routes: RouteRecordRaw[] = [ middlewareOptions: { custom: () => { const settingsStore = useSettingsStore(); - return settingsStore.isAiGatewayEnabled; + return settingsStore.isAiGatewayEnabled && !settingsStore.isAiGatewayCloudUbbEnabled; }, }, telemetry: {