fix(editor): Move versions check to init function and refactor store (no-changelog) (#8067)

This commit is contained in:
Alex Grozav 2023-12-20 12:49:40 +02:00 committed by GitHub
parent faadfd6d4a
commit fcff34c401
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 223 additions and 84 deletions

View file

@ -0,0 +1,32 @@
/**
* Getters
*/
export function getVersionUpdatesPanelOpenButton() {
return cy.getByTestId('version-updates-panel-button');
}
export function getVersionUpdatesPanel() {
return cy.getByTestId('version-updates-panel');
}
export function getVersionUpdatesPanelCloseButton() {
return getVersionUpdatesPanel().get('.el-drawer__close-btn').first();
}
export function getVersionCard() {
return cy.getByTestId('version-card');
}
/**
* Actions
*/
export function openVersionUpdatesPanel() {
getVersionUpdatesPanelOpenButton().click();
getVersionUpdatesPanel().should('be.visible');
}
export function closeVersionUpdatesPanel() {
getVersionUpdatesPanelCloseButton().click();
}

View file

@ -0,0 +1,66 @@
import { INSTANCE_OWNER } from '../constants';
import { WorkflowsPage } from '../pages/workflows';
import {
closeVersionUpdatesPanel,
getVersionCard,
getVersionUpdatesPanelOpenButton,
openVersionUpdatesPanel,
} from '../composables/versions';
const workflowsPage = new WorkflowsPage();
describe('Versions', () => {
it('should open updates panel', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.continue((res) => {
if (res.body.hasOwnProperty('data')) {
res.body.data = {
...res.body.data,
releaseChannel: 'stable',
versionCli: '1.0.0',
versionNotifications: {
enabled: true,
endpoint: 'https://api.n8n.io/api/versions/',
infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html',
},
};
}
});
}).as('settings');
cy.intercept('GET', 'https://api.n8n.io/api/versions/1.0.0', [
{
name: '1.3.1',
createdAt: '2023-08-18T11:53:12.857Z',
hasSecurityIssue: null,
hasSecurityFix: null,
securityIssueFixVersion: null,
hasBreakingChange: null,
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n131',
nodes: [],
description: 'Includes <strong>bug fixes</strong>',
},
{
name: '1.0.5',
createdAt: '2023-07-24T10:54:56.097Z',
hasSecurityIssue: false,
hasSecurityFix: null,
securityIssueFixVersion: null,
hasBreakingChange: true,
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n104',
nodes: [],
description: 'Includes <strong>core functionality</strong> and <strong>bug fixes</strong>',
},
]);
cy.signin(INSTANCE_OWNER);
cy.visit(workflowsPage.url);
cy.wait('@settings');
getVersionUpdatesPanelOpenButton().should('contain', '2 updates');
openVersionUpdatesPanel();
getVersionCard().should('have.length', 2);
closeVersionUpdatesPanel();
});
});

View file

@ -35,7 +35,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { newVersions } from '@/mixins/newVersions';
import BannerStack from '@/components/banners/BannerStack.vue'; import BannerStack from '@/components/banners/BannerStack.vue';
import Modals from '@/components/Modals.vue'; import Modals from '@/components/Modals.vue';
@ -69,15 +68,13 @@ export default defineComponent({
Telemetry, Telemetry,
Modals, Modals,
}, },
mixins: [newVersions, userHelpers], mixins: [userHelpers],
setup(props) { setup() {
return { return {
...useGlobalLinkActions(), ...useGlobalLinkActions(),
...useHistoryHelper(useRoute()), ...useHistoryHelper(useRoute()),
...useToast(), ...useToast(),
externalHooks: useExternalHooks(), externalHooks: useExternalHooks(),
// eslint-disable-next-line @typescript-eslint/no-misused-promises
...newVersions.setup?.(props),
}; };
}, },
computed: { computed: {
@ -115,7 +112,6 @@ export default defineComponent({
async mounted() { async mounted() {
this.logHiringBanner(); this.logHiringBanner();
void this.checkForNewVersions();
void initializeAuthenticatedFeatures(); void initializeAuthenticatedFeatures();
void useExternalHooks().run('app.mount'); void useExternalHooks().run('app.mount');

View file

@ -3,14 +3,14 @@ import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useRootStore } from '@/stores/n8nRoot.store'; import { useRootStore } from '@/stores/n8nRoot.store';
import { initializeAuthenticatedFeatures } from '@/init'; import { initializeAuthenticatedFeatures, initializeCore } from '@/init';
import type { SpyInstance } from 'vitest';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia'; import { setActivePinia } from 'pinia';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useVersionsStore } from '@/stores/versions.store';
vi.mock('@/stores/users.store', () => ({ vi.mock('@/stores/users.store', () => ({
useUsersStore: vi.fn(), useUsersStore: vi.fn().mockReturnValue({ initialize: vi.fn() }),
})); }));
vi.mock('@/stores/n8nRoot.store', () => ({ vi.mock('@/stores/n8nRoot.store', () => ({
@ -18,22 +18,48 @@ vi.mock('@/stores/n8nRoot.store', () => ({
})); }));
describe('Init', () => { describe('Init', () => {
describe('Authenticated Features', () => { let settingsStore: ReturnType<typeof useSettingsStore>;
let settingsStore: ReturnType<typeof useSettingsStore>; let cloudPlanStore: ReturnType<typeof useCloudPlanStore>;
let cloudPlanStore: ReturnType<typeof useCloudPlanStore>; let sourceControlStore: ReturnType<typeof useSourceControlStore>;
let sourceControlStore: ReturnType<typeof useSourceControlStore>; let usersStore: ReturnType<typeof useUsersStore>;
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>; let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
let cloudStoreSpy: SpyInstance<[], Promise<void>>; let versionsStore: ReturnType<typeof useVersionsStore>;
let templatesTestSpy: SpyInstance<[], Promise<void>>;
let sourceControlSpy: SpyInstance<[], Promise<void>>;
let nodeTranslationSpy: SpyInstance<[], Promise<void>>;
beforeAll(() => { beforeEach(() => {
setActivePinia(createTestingPinia()); setActivePinia(createTestingPinia());
settingsStore = useSettingsStore(); settingsStore = useSettingsStore();
cloudPlanStore = useCloudPlanStore(); cloudPlanStore = useCloudPlanStore();
sourceControlStore = useSourceControlStore(); sourceControlStore = useSourceControlStore();
nodeTypesStore = useNodeTypesStore(); nodeTypesStore = useNodeTypesStore();
usersStore = useUsersStore();
versionsStore = useVersionsStore();
versionsStore = useVersionsStore();
});
describe('initializeCore()', () => {
afterEach(() => {
vi.clearAllMocks();
});
it('should initialize core features only once', async () => {
const usersStoreSpy = vi.spyOn(usersStore, 'initialize');
const settingsStoreSpy = vi.spyOn(settingsStore, 'initialize');
const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions');
await initializeCore();
expect(settingsStoreSpy).toHaveBeenCalled();
expect(usersStoreSpy).toHaveBeenCalled();
expect(versionsSpy).toHaveBeenCalled();
await initializeCore();
expect(settingsStoreSpy).toHaveBeenCalledTimes(1);
});
});
describe('initializeAuthenticatedFeatures()', () => {
beforeEach(() => {
vi.spyOn(settingsStore, 'isCloudDeployment', 'get').mockReturnValue(true); vi.spyOn(settingsStore, 'isCloudDeployment', 'get').mockReturnValue(true);
vi.spyOn(settingsStore, 'isTemplatesEnabled', 'get').mockReturnValue(true); vi.spyOn(settingsStore, 'isTemplatesEnabled', 'get').mockReturnValue(true);
vi.spyOn(sourceControlStore, 'isEnterpriseSourceControlEnabled', 'get').mockReturnValue(true); vi.spyOn(sourceControlStore, 'isEnterpriseSourceControlEnabled', 'get').mockReturnValue(true);
@ -43,10 +69,6 @@ describe('Init', () => {
vi.mock('@/hooks/register', () => ({ vi.mock('@/hooks/register', () => ({
initializeCloudHooks: vi.fn(), initializeCloudHooks: vi.fn(),
})); }));
cloudStoreSpy = vi.spyOn(cloudPlanStore, 'initialize');
templatesTestSpy = vi.spyOn(settingsStore, 'testTemplatesEndpoint');
sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences');
nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders');
}); });
afterEach(() => { afterEach(() => {
@ -54,24 +76,40 @@ describe('Init', () => {
}); });
it('should not init authenticated features if user is not logged in', async () => { it('should not init authenticated features if user is not logged in', async () => {
const cloudStoreSpy = vi.spyOn(cloudPlanStore, 'initialize');
const templatesTestSpy = vi.spyOn(settingsStore, 'testTemplatesEndpoint');
const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences');
const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders');
vi.mocked(useUsersStore).mockReturnValue({ currentUser: null } as ReturnType< vi.mocked(useUsersStore).mockReturnValue({ currentUser: null } as ReturnType<
typeof useUsersStore typeof useUsersStore
>); >);
await initializeAuthenticatedFeatures(); await initializeAuthenticatedFeatures();
expect(cloudStoreSpy).not.toHaveBeenCalled(); expect(cloudStoreSpy).not.toHaveBeenCalled();
expect(templatesTestSpy).not.toHaveBeenCalled(); expect(templatesTestSpy).not.toHaveBeenCalled();
expect(sourceControlSpy).not.toHaveBeenCalled(); expect(sourceControlSpy).not.toHaveBeenCalled();
expect(nodeTranslationSpy).not.toHaveBeenCalled(); expect(nodeTranslationSpy).not.toHaveBeenCalled();
}); });
it('should init authenticated features if user is not logged in', async () => {
it('should init authenticated features only once if user is logged in', async () => {
const cloudStoreSpy = vi.spyOn(cloudPlanStore, 'initialize');
const templatesTestSpy = vi.spyOn(settingsStore, 'testTemplatesEndpoint');
const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences');
const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders');
vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType< vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType<
typeof useUsersStore typeof useUsersStore
>); >);
await initializeAuthenticatedFeatures(); await initializeAuthenticatedFeatures();
expect(cloudStoreSpy).toHaveBeenCalled(); expect(cloudStoreSpy).toHaveBeenCalled();
expect(templatesTestSpy).toHaveBeenCalled(); expect(templatesTestSpy).toHaveBeenCalled();
expect(sourceControlSpy).toHaveBeenCalled(); expect(sourceControlSpy).toHaveBeenCalled();
expect(nodeTranslationSpy).toHaveBeenCalled(); expect(nodeTranslationSpy).toHaveBeenCalled();
await initializeAuthenticatedFeatures();
expect(cloudStoreSpy).toHaveBeenCalledTimes(1);
}); });
}); });
}); });

View file

@ -29,7 +29,12 @@
/></template> /></template>
<template #menuSuffix> <template #menuSuffix>
<div> <div>
<div v-if="hasVersionUpdates" :class="$style.updates" @click="openUpdatesPanel"> <div
v-if="hasVersionUpdates"
data-test-id="version-updates-panel-button"
:class="$style.updates"
@click="openUpdatesPanel"
>
<div :class="$style.giftContainer"> <div :class="$style.giftContainer">
<GiftNotificationIcon /> <GiftNotificationIcon />
</div> </div>

View file

@ -1,5 +1,10 @@
<template> <template>
<ModalDrawer :name="VERSIONS_MODAL_KEY" direction="ltr" width="520px"> <ModalDrawer
:name="VERSIONS_MODAL_KEY"
direction="ltr"
width="520px"
data-test-id="version-updates-panel"
>
<template #header> <template #header>
<span :class="$style.title"> <span :class="$style.title">
{{ $locale.baseText('updatesPanel.weVeBeenBusy') }} {{ $locale.baseText('updatesPanel.weVeBeenBusy') }}
@ -31,7 +36,7 @@
</p> </p>
<n8n-link v-if="infoUrl" :to="infoUrl" :bold="true"> <n8n-link v-if="infoUrl" :to="infoUrl" :bold="true">
<font-awesome-icon icon="info-circle"></font-awesome-icon> <font-awesome-icon icon="info-circle" class="mr-2xs" />
<span> <span>
{{ $locale.baseText('updatesPanel.howToUpdateYourN8nVersion') }} {{ $locale.baseText('updatesPanel.howToUpdateYourN8nVersion') }}
</span> </span>

View file

@ -1,5 +1,11 @@
<template> <template>
<a v-if="version" :href="version.documentationUrl" target="_blank" :class="$style.card"> <a
v-if="version"
:href="version.documentationUrl"
target="_blank"
:class="$style.card"
data-test-id="version-card"
>
<div :class="$style.header"> <div :class="$style.header">
<div> <div>
<div :class="$style.name"> <div :class="$style.name">

View file

@ -5,6 +5,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { initializeCloudHooks } from '@/hooks/register'; import { initializeCloudHooks } from '@/hooks/register';
import { useVersionsStore } from '@/stores/versions.store';
let coreInitialized = false; let coreInitialized = false;
let authenticatedFeaturesInitialized = false; let authenticatedFeaturesInitialized = false;
@ -20,10 +21,13 @@ export async function initializeCore() {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const usersStore = useUsersStore(); const usersStore = useUsersStore();
const versionsStore = useVersionsStore();
await settingsStore.initialize(); await settingsStore.initialize();
await usersStore.initialize(); await usersStore.initialize();
void versionsStore.checkForNewVersions();
if (settingsStore.isCloudDeployment) { if (settingsStore.isCloudDeployment) {
try { try {
await initializeCloudHooks(); await initializeCloudHooks();

View file

@ -1,50 +0,0 @@
import { defineComponent } from 'vue';
import { useToast } from '@/composables/useToast';
import { VERSIONS_MODAL_KEY } from '@/constants';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui.store';
import { useVersionsStore } from '@/stores/versions.store';
export const newVersions = defineComponent({
setup() {
return {
...useToast(),
};
},
computed: {
...mapStores(useUIStore, useVersionsStore),
},
methods: {
async checkForNewVersions() {
const enabled = this.versionsStore.areNotificationsEnabled;
if (!enabled) {
return;
}
await this.versionsStore.fetchVersions();
const currentVersion = this.versionsStore.currentVersion;
const nextVersions = this.versionsStore.nextVersions;
if (currentVersion && currentVersion.hasSecurityIssue && nextVersions.length) {
const fixVersion = currentVersion.securityIssueFixVersion;
let message = 'Please update to latest version.';
if (fixVersion) {
message = `Please update to version ${fixVersion} or higher.`;
}
message = `${message} <a class="primary-color">More info</a>`;
this.showToast({
title: 'Critical update available',
message,
onClick: () => {
this.uiStore.openModal(VERSIONS_MODAL_KEY);
},
closeOnClick: true,
customClass: 'clickable',
type: 'warning',
duration: 0,
});
}
},
},
});

View file

@ -1,8 +1,10 @@
import { getNextVersions } from '@/api/versions'; import { getNextVersions } from '@/api/versions';
import { STORES } from '@/constants'; import { STORES, VERSIONS_MODAL_KEY } from '@/constants';
import type { IVersion, IVersionNotificationSettings, IVersionsState } from '@/Interface'; import type { IVersion, IVersionNotificationSettings, IVersionsState } from '@/Interface';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useRootStore } from './n8nRoot.store'; import { useRootStore } from './n8nRoot.store';
import { useToast } from '@/composables/useToast';
import { useUIStore } from '@/stores/ui.store';
export const useVersionsStore = defineStore(STORES.VERSIONS, { export const useVersionsStore = defineStore(STORES.VERSIONS, {
state: (): IVersionsState => ({ state: (): IVersionsState => ({
@ -45,5 +47,40 @@ export const useVersionsStore = defineStore(STORES.VERSIONS, {
} }
} catch (e) {} } catch (e) {}
}, },
async checkForNewVersions() {
const enabled = this.areNotificationsEnabled;
if (!enabled) {
return;
}
const { showToast } = useToast();
const uiStore = useUIStore();
await this.fetchVersions();
const currentVersion = this.currentVersion;
const nextVersions = this.nextVersions;
if (currentVersion && currentVersion.hasSecurityIssue && nextVersions.length) {
const fixVersion = currentVersion.securityIssueFixVersion;
let message = 'Please update to latest version.';
if (fixVersion) {
message = `Please update to version ${fixVersion} or higher.`;
}
message = `${message} <a class="primary-color">More info</a>`;
showToast({
title: 'Critical update available',
message,
onClick: () => {
uiStore.openModal(VERSIONS_MODAL_KEY);
},
closeOnClick: true,
customClass: 'clickable',
type: 'warning',
duration: 0,
});
}
},
}, },
}); });