Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(editor): Fix cloud plan data loading on instance #7841

Merged
merged 12 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions packages/editor-ui/src/__tests__/init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useUsersStore } from '@/stores/users.store';
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useRootStore } from '@/stores/n8nRoot.store';
import { initializeAuthenticatedFeatures } from '@/init';
import type { SpyInstance } from 'vitest';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { useSettingsStore } from '@/stores/settings.store';

vi.mock('@/stores/users.store', () => ({
useUsersStore: vi.fn(),
}));

vi.mock('@/stores/n8nRoot.store', () => ({
useRootStore: vi.fn(),
}));

describe('Init', () => {
describe('Authenticated Features', () => {
let settingsStore: ReturnType<typeof useSettingsStore>;
let cloudPlanStore: ReturnType<typeof useCloudPlanStore>;
let sourceControlStore: ReturnType<typeof useSourceControlStore>;
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
let cloudStoreSpy: SpyInstance<[], Promise<void>>;
let templatesTestSpy: SpyInstance<[], Promise<void>>;
let sourceControlSpy: SpyInstance<[], Promise<void>>;
let nodeTranslationSpy: SpyInstance<[], Promise<void>>;

beforeAll(() => {
setActivePinia(createTestingPinia());
settingsStore = useSettingsStore();
cloudPlanStore = useCloudPlanStore();
sourceControlStore = useSourceControlStore();
nodeTypesStore = useNodeTypesStore();
vi.spyOn(settingsStore, 'isCloudDeployment', 'get').mockReturnValue(true);
vi.spyOn(settingsStore, 'isTemplatesEnabled', 'get').mockReturnValue(true);
vi.spyOn(sourceControlStore, 'isEnterpriseSourceControlEnabled', 'get').mockReturnValue(true);
vi.mocked(useRootStore).mockReturnValue({ defaultLocale: 'es' } as ReturnType<
typeof useRootStore
>);
vi.mock('@/hooks/register', () => ({
initializeCloudHooks: vi.fn(),
}));
cloudStoreSpy = vi.spyOn(cloudPlanStore, 'initialize');
templatesTestSpy = vi.spyOn(settingsStore, 'testTemplatesEndpoint');
sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences');
nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders');
});

afterEach(() => {
vi.clearAllMocks();
});

it('should not init authenticated features if user is not logged in', async () => {
vi.mocked(useUsersStore).mockReturnValue({ currentUser: null } as ReturnType<
typeof useUsersStore
>);
await initializeAuthenticatedFeatures();
expect(cloudStoreSpy).not.toHaveBeenCalled();
expect(templatesTestSpy).not.toHaveBeenCalled();
expect(sourceControlSpy).not.toHaveBeenCalled();
expect(nodeTranslationSpy).not.toHaveBeenCalled();
});
it('should init authenticated features if user is not logged in', async () => {
vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType<
typeof useUsersStore
>);
await initializeAuthenticatedFeatures();
expect(cloudStoreSpy).toHaveBeenCalled();
expect(templatesTestSpy).toHaveBeenCalled();
expect(sourceControlSpy).toHaveBeenCalled();
expect(nodeTranslationSpy).toHaveBeenCalled();
});
});
});
11 changes: 8 additions & 3 deletions packages/editor-ui/src/hooks/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ export async function initializeCloudHooks() {
return;
}

const { n8nCloudHooks } = await import('@/hooks/cloud');
extendExternalHooks(n8nCloudHooks);
cloudHooksInitialized = true;
try {
const { n8nCloudHooks } = await import('@/hooks/cloud');
extendExternalHooks(n8nCloudHooks);
} catch (error) {
throw new Error(`Failed to extend external hooks: ${error.message}`);
} finally {
cloudHooksInitialized = true;
}
}
17 changes: 15 additions & 2 deletions packages/editor-ui/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ export async function initializeCore() {
}

const settingsStore = useSettingsStore();
const cloudPlanStore = useCloudPlanStore();
const usersStore = useUsersStore();

await settingsStore.initialize();
await usersStore.initialize();

if (settingsStore.isCloudDeployment) {
await Promise.all([cloudPlanStore.initialize(), initializeCloudHooks()]);
try {
await initializeCloudHooks();
} catch (e) {
console.error('Failed to initialize cloud hooks:', e);
}
}

coreInitialized = true;
Expand All @@ -48,6 +52,7 @@ export async function initializeAuthenticatedFeatures() {
const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const nodeTypesStore = useNodeTypesStore();
const cloudPlanStore = useCloudPlanStore();

if (sourceControlStore.isEnterpriseSourceControlEnabled) {
await sourceControlStore.getPreferences();
Expand All @@ -63,5 +68,13 @@ export async function initializeAuthenticatedFeatures() {
await nodeTypesStore.getNodeTranslationHeaders();
}

if (settingsStore.isCloudDeployment) {
try {
await cloudPlanStore.initialize();
} catch (e) {
console.error('Failed to initialize cloud plan store:', e);
}
}

authenticatedFeaturesInitialized = true;
}
9 changes: 9 additions & 0 deletions packages/editor-ui/src/stores/__tests__/ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,15 @@ describe('UI store', () => {
const fetchUserCloudAccountSpy = vi
.spyOn(cloudPlanApi, 'getCloudUserInfo')
.mockResolvedValue(getUserCloudInfo(true));
const getCurrentUsageSpy = vi
.spyOn(cloudPlanApi, 'getCurrentUsage')
.mockResolvedValue({ executions: 1000, activeWorkflows: 100 });
setupOwnerAndCloudDeployment();
await cloudPlanStore.checkForCloudPlanData();
await cloudPlanStore.fetchUserCloudAccount();
expect(fetchCloudSpy).toHaveBeenCalled();
expect(fetchUserCloudAccountSpy).toHaveBeenCalled();
expect(getCurrentUsageSpy).toHaveBeenCalled();
expect(uiStore.bannerStack).toContain('TRIAL');
// There should be no email confirmation banner for trialing users
expect(uiStore.bannerStack).not.toContain('EMAIL_CONFIRMATION');
Expand All @@ -183,10 +187,15 @@ describe('UI store', () => {
.spyOn(cloudPlanApi, 'getCloudUserInfo')
.mockResolvedValue(getUserCloudInfo(true));
setupOwnerAndCloudDeployment();
const getCurrentUsageSpy = vi
.spyOn(cloudPlanApi, 'getCurrentUsage')
.mockResolvedValue({ executions: 1000, activeWorkflows: 100 });
setupOwnerAndCloudDeployment();
await cloudPlanStore.checkForCloudPlanData();
await cloudPlanStore.fetchUserCloudAccount();
expect(fetchCloudSpy).toHaveBeenCalled();
expect(fetchUserCloudAccountSpy).toHaveBeenCalled();
expect(getCurrentUsageSpy).toHaveBeenCalled();
expect(uiStore.bannerStack).toContain('TRIAL_OVER');
// There should be no email confirmation banner for trialing users
expect(uiStore.bannerStack).not.toContain('EMAIL_CONFIRMATION');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function getUserPlanData(trialExpirationDate: Date, isTrial = true): Cloud.PlanD
isActive: true,
displayName: 'Trial',
metadata: {
group: isTrial ? 'trial' : 'pro',
group: isTrial ? 'trial' : 'opt-in',
slug: 'trial-1',
trial: {
gracePeriod: 3,
Expand Down
25 changes: 19 additions & 6 deletions packages/editor-ui/src/stores/cloudPlan.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {

const hasCloudPlan = computed(() => {
const cloudUserId = settingsStore.settings.n8nMetadata?.userId;
return usersStore.currentUser?.isOwner && settingsStore.isCloudDeployment && cloudUserId;
return usersStore.isInstanceOwner && settingsStore.isCloudDeployment && cloudUserId;
});

const getUserCloudAccount = async () => {
Expand All @@ -68,7 +68,7 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
}
}
} catch (error) {
throw new Error(error);
throw new Error(error.message);
}
};

Expand Down Expand Up @@ -143,13 +143,17 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
if (!userIsTrialing.value) return;
await getInstanceCurrentUsage();
startPollingInstanceUsageData();
} catch {}
} catch (e) {
throw new Error(e.message);
}
};

const fetchUserCloudAccount = async () => {
try {
await getUserCloudAccount();
} catch {}
} catch (e) {
throw new Error(e.message);
}
};

const redirectToDashboard = async () => {
Expand All @@ -163,8 +167,17 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
return;
}

await checkForCloudPlanData();
await fetchUserCloudAccount();
try {
await checkForCloudPlanData();
} catch (error) {
console.warn('Error checking for cloud plan data:', error);
}

try {
await fetchUserCloudAccount();
} catch (error) {
console.warn('Error fetching user cloud account:', error);
}

state.initialized = true;
};
Expand Down
Loading