feat: Hide n8n-connect usage when aiGatewayCloudUbb is enabled (#35978)

This commit is contained in:
Ragnar 2026-08-11 12:13:43 +02:00 committed by GitHub
parent 88efdb975e
commit e7fa25217e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 89 additions and 3 deletions

View File

@ -247,6 +247,7 @@ export interface FrontendSettings {
aiGateway?: {
enabled: boolean;
budget: number;
cloudUbbEnabled: boolean;
};
ai: {
allowSendingParameterValues: boolean;

View File

@ -114,6 +114,10 @@ export class LicenseState {
return this.isLicensed('feat:aiGateway');
}
isAiGatewayCloudUbbLicensed() {
return this.isLicensed('feat:aiGatewayCloudUbb');
}
isAdvancedExecutionFiltersLicensed() {
return this.isLicensed('feat:advancedExecutionFilters');
}

View File

@ -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',

View File

@ -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,

View File

@ -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';

View File

@ -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(),
};
}

View File

@ -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,

View File

@ -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: {} });
});

View File

@ -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);
});
});

View File

@ -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

View File

@ -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')

View File

@ -768,7 +768,7 @@ export const routes: RouteRecordRaw[] = [
middlewareOptions: {
custom: () => {
const settingsStore = useSettingsStore();
return settingsStore.isAiGatewayEnabled;
return settingsStore.isAiGatewayEnabled && !settingsStore.isAiGatewayCloudUbbEnabled;
},
},
telemetry: {