refactor(editor): Migrate settings.store to composition API (no-changelog) (#10022)

Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
Ricardo Espinoza 2024-07-19 08:35:36 -04:00 committed by GitHub
parent 062633ec9b
commit ba27c987dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 585 additions and 514 deletions

View file

@ -23,7 +23,7 @@ export const defaultSettings: IN8nUISettings = {
logStreaming: false, logStreaming: false,
debugInEditor: false, debugInEditor: false,
advancedExecutionFilters: false, advancedExecutionFilters: false,
variables: true, variables: false,
sourceControl: false, sourceControl: false,
auditLogs: false, auditLogs: false,
showNonProdBanner: false, showNonProdBanner: false,

View file

@ -2,9 +2,14 @@ import { createComponentRenderer } from '@/__tests__/render';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import Assignment from '../Assignment.vue'; import Assignment from '../Assignment.vue';
import { defaultSettings } from '@/__tests__/defaults';
import { STORES } from '@/constants';
import { merge } from 'lodash-es';
const DEFAULT_SETUP = { const DEFAULT_SETUP = {
pinia: createTestingPinia(), pinia: createTestingPinia({
initialState: { [STORES.SETTINGS]: { settings: merge({}, defaultSettings) } },
}),
props: { props: {
path: 'parameters.fields.0', path: 'parameters.fields.0',
modelValue: { modelValue: {

View file

@ -5,10 +5,16 @@ import userEvent from '@testing-library/user-event';
import { fireEvent, within } from '@testing-library/vue'; import { fireEvent, within } from '@testing-library/vue';
import * as workflowHelpers from '@/composables/useWorkflowHelpers'; import * as workflowHelpers from '@/composables/useWorkflowHelpers';
import AssignmentCollection from '../AssignmentCollection.vue'; import AssignmentCollection from '../AssignmentCollection.vue';
import { createPinia, setActivePinia } from 'pinia'; import { STORES } from '@/constants';
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
const DEFAULT_SETUP = { const DEFAULT_SETUP = {
pinia: createTestingPinia(), pinia: createTestingPinia({
initialState: {
[STORES.SETTINGS]: SETTINGS_STORE_DEFAULT_STATE,
},
stubActions: false,
}),
props: { props: {
path: 'parameters.fields', path: 'parameters.fields',
node: { node: {
@ -97,10 +103,7 @@ describe('AssignmentCollection.vue', () => {
}); });
it('can add assignments by drag and drop (and infer type)', async () => { it('can add assignments by drag and drop (and infer type)', async () => {
const pinia = createPinia(); const { getByTestId, findAllByTestId } = renderComponent();
setActivePinia(pinia);
const { getByTestId, findAllByTestId } = renderComponent({ pinia });
const dropArea = getByTestId('drop-area'); const dropArea = getByTestId('drop-area');
await dropAssignment({ key: 'boolKey', value: true, dropArea }); await dropAssignment({ key: 'boolKey', value: true, dropArea });

View file

@ -780,7 +780,7 @@ async function saveCredential(): Promise<ICredentialsResponse | null> {
}; };
if ( if (
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing) && settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing] &&
credentialData.value.sharedWithProjects credentialData.value.sharedWithProjects
) { ) {
credentialDetails.sharedWithProjects = credentialData.value credentialDetails.sharedWithProjects = credentialData.value
@ -817,7 +817,7 @@ async function saveCredential(): Promise<ICredentialsResponse | null> {
type: 'success', type: 'success',
}); });
} else { } else {
if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) { if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]) {
credentialDetails.sharedWithProjects = credentialData.value credentialDetails.sharedWithProjects = credentialData.value
.sharedWithProjects as ProjectSharingData[]; .sharedWithProjects as ProjectSharingData[];
} }

View file

@ -156,7 +156,7 @@ export default defineComponent({
]; ];
}, },
isSharingEnabled(): boolean { isSharingEnabled(): boolean {
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing); return this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing];
}, },
credentialOwnerName(): string { credentialOwnerName(): string {
return this.credentialsStore.getCredentialOwnerNameById(`${this.credentialId}`); return this.credentialsStore.getCredentialOwnerNameById(`${this.credentialId}`);

View file

@ -23,7 +23,7 @@ export default defineComponent({
...mapStores(useSettingsStore), ...mapStores(useSettingsStore),
canAccess(): boolean { canAccess(): boolean {
return this.features.reduce((acc: boolean, feature) => { return this.features.reduce((acc: boolean, feature) => {
return acc && !!this.settingsStore.isEnterpriseFeatureEnabled(feature); return acc && !!this.settingsStore.isEnterpriseFeatureEnabled[feature];
}, true); }, true);
}, },
}, },

View file

@ -190,9 +190,9 @@ export default defineComponent({
: []; : [];
}, },
isAdvancedPermissionsEnabled(): boolean { isAdvancedPermissionsEnabled(): boolean {
return this.settingsStore.isEnterpriseFeatureEnabled( return this.settingsStore.isEnterpriseFeatureEnabled[
EnterpriseEditionFeature.AdvancedPermissions, EnterpriseEditionFeature.AdvancedPermissions
); ];
}, },
}, },
methods: { methods: {

View file

@ -208,7 +208,7 @@ const workflowMenuItems = computed<ActionDropdownItem[]>(() => {
}); });
const isWorkflowHistoryFeatureEnabled = computed(() => { const isWorkflowHistoryFeatureEnabled = computed(() => {
return settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowHistory); return settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.WorkflowHistory];
}); });
const workflowHistoryRoute = computed<{ name: string; params: { workflowId: string } }>(() => { const workflowHistoryRoute = computed<{ name: string; params: { workflowId: string } }>(() => {

View file

@ -439,7 +439,7 @@ const foreignCredentials = computed(() => {
const usedCredentials = workflowsStore.usedCredentials; const usedCredentials = workflowsStore.usedCredentials;
const foreignCredentialsArray: string[] = []; const foreignCredentialsArray: string[] = [];
if (credentials && settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) { if (credentials && settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]) {
Object.values(credentials).forEach((credential) => { Object.values(credentials).forEach((credential) => {
if ( if (
credential.id && credential.id &&

View file

@ -50,8 +50,8 @@ const valueInputRef = ref<HTMLElement>();
const usage = ref(`$vars.${props.data.name}`); const usage = ref(`$vars.${props.data.name}`);
const isFeatureEnabled = computed(() => const isFeatureEnabled = computed(
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Variables), () => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Variables],
); );
onMounted(() => { onMounted(() => {

View file

@ -480,7 +480,7 @@ export default defineComponent({
return this.usersStore.currentUser; return this.usersStore.currentUser;
}, },
isSharingEnabled(): boolean { isSharingEnabled(): boolean {
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing); return this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing];
}, },
workflowOwnerName(): string { workflowOwnerName(): string {
const fallback = this.$locale.baseText( const fallback = this.$locale.baseText(

View file

@ -197,7 +197,7 @@ export default defineComponent({
useRolesStore, useRolesStore,
), ),
isSharingEnabled(): boolean { isSharingEnabled(): boolean {
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing); return this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing];
}, },
modalTitle(): string { modalTitle(): string {
if (this.isHomeTeamProject) { if (this.isHomeTeamProject) {

View file

@ -43,8 +43,8 @@ const debouncedEmit = debounce(emit, {
}); });
const isCustomDataFilterTracked = ref(false); const isCustomDataFilterTracked = ref(false);
const isAdvancedExecutionFilterEnabled = computed(() => const isAdvancedExecutionFilterEnabled = computed(
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.AdvancedExecutionFilters), () => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.AdvancedExecutionFilters],
); );
const showTags = computed(() => false); const showTags = computed(() => false);

View file

@ -1,3 +1,4 @@
import { useRootStore } from '@/stores/root.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import type { WorkflowSettings } from 'n8n-workflow'; import type { WorkflowSettings } from 'n8n-workflow';
@ -38,60 +39,62 @@ type DebugInfo = {
}; };
export function useDebugInfo() { export function useDebugInfo() {
const store = useSettingsStore(); const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const coreInfo = () => { const coreInfo = () => {
return { return {
n8nVersion: store.versionCli, n8nVersion: rootStore.versionCli,
platform: platform:
store.isDocker && store.deploymentType === 'cloud' settingsStore.isDocker && settingsStore.deploymentType === 'cloud'
? 'docker (cloud)' ? 'docker (cloud)'
: store.isDocker : settingsStore.isDocker
? 'docker (self-hosted)' ? 'docker (self-hosted)'
: 'npm', : 'npm',
nodeJsVersion: store.nodeJsVersion, nodeJsVersion: settingsStore.nodeJsVersion,
database: database:
store.databaseType === 'postgresdb' settingsStore.databaseType === 'postgresdb'
? 'postgres' ? 'postgres'
: store.databaseType === 'mysqldb' : settingsStore.databaseType === 'mysqldb'
? 'mysql' ? 'mysql'
: store.databaseType, : settingsStore.databaseType,
executionMode: store.isQueueModeEnabled ? 'scaling' : 'regular', executionMode: settingsStore.isQueueModeEnabled ? 'scaling' : 'regular',
concurrency: store.settings.concurrency, concurrency: settingsStore.settings.concurrency,
license: store.isCommunityPlan license: settingsStore.isCommunityPlan
? 'community' ? 'community'
: store.settings.license.environment === 'production' : settingsStore.settings.license.environment === 'production'
? 'enterprise (production)' ? 'enterprise (production)'
: 'enterprise (sandbox)', : 'enterprise (sandbox)',
consumerId: store.consumerId, consumerId: settingsStore.consumerId,
} as const; } as const;
}; };
const storageInfo = (): DebugInfo['storage'] => { const storageInfo = (): DebugInfo['storage'] => {
return { return {
success: store.saveDataSuccessExecution, success: settingsStore.saveDataSuccessExecution,
error: store.saveDataErrorExecution, error: settingsStore.saveDataErrorExecution,
progress: store.saveDataProgressExecution, progress: settingsStore.saveDataProgressExecution,
manual: store.saveManualExecutions, manual: settingsStore.saveManualExecutions,
binaryMode: store.binaryDataMode === 'default' ? 'memory' : store.binaryDataMode, binaryMode:
settingsStore.binaryDataMode === 'default' ? 'memory' : settingsStore.binaryDataMode,
}; };
}; };
const pruningInfo = () => { const pruningInfo = () => {
if (!store.pruning.isEnabled) return { enabled: false } as const; if (!settingsStore.pruning.isEnabled) return { enabled: false } as const;
return { return {
enabled: true, enabled: true,
maxAge: `${store.pruning.maxAge} hours`, maxAge: `${settingsStore.pruning.maxAge} hours`,
maxCount: `${store.pruning.maxCount} executions`, maxCount: `${settingsStore.pruning.maxCount} executions`,
} as const; } as const;
}; };
const securityInfo = () => { const securityInfo = () => {
const info: DebugInfo['security'] = {}; const info: DebugInfo['security'] = {};
if (!store.security.blockFileAccessToN8nFiles) info.blockFileAccessToN8nFiles = false; if (!settingsStore.security.blockFileAccessToN8nFiles) info.blockFileAccessToN8nFiles = false;
if (!store.security.secureCookie) info.secureCookie = false; if (!settingsStore.security.secureCookie) info.secureCookie = false;
if (Object.keys(info).length === 0) return; if (Object.keys(info).length === 0) return;

View file

@ -28,8 +28,8 @@ export const useExecutionDebugging = () => {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const uiStore = useUIStore(); const uiStore = useUIStore();
const isDebugEnabled = computed(() => const isDebugEnabled = computed(
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.DebugInEditor), () => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.DebugInEditor],
); );
const applyExecutionData = async (executionId: string): Promise<void> => { const applyExecutionData = async (executionId: string): Promise<void> => {

View file

@ -44,7 +44,7 @@ function setCurrentUser() {
} }
function resetStores() { function resetStores() {
useSettingsStore().$reset(); useSettingsStore().reset();
useUsersStore().reset(); useUsersStore().reset();
} }

View file

@ -3,9 +3,9 @@ import { generateUpgradeLinkUrl, useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { merge } from 'lodash-es'; import { merge } from 'lodash-es';
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
import { useCloudPlanStore } from '@/stores/cloudPlan.store'; import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import * as cloudPlanApi from '@/api/cloudPlans'; import * as cloudPlanApi from '@/api/cloudPlans';
import { defaultSettings } from '../../__tests__/defaults';
import { import {
getTrialExpiredUserResponse, getTrialExpiredUserResponse,
getTrialingUserResponse, getTrialingUserResponse,
@ -34,7 +34,7 @@ function setUser(role: IRole) {
function setupOwnerAndCloudDeployment() { function setupOwnerAndCloudDeployment() {
setUser(ROLE.Owner); setUser(ROLE.Owner);
settingsStore.setSettings( settingsStore.setSettings(
merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, { merge({}, defaultSettings, {
n8nMetadata: { n8nMetadata: {
userId: '1', userId: '1',
}, },
@ -103,7 +103,7 @@ describe('UI store', () => {
setUser(role as IRole); setUser(role as IRole);
settingsStore.setSettings( settingsStore.setSettings(
merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, { merge({}, defaultSettings, {
deployment: { deployment: {
type, type,
}, },
@ -123,7 +123,7 @@ describe('UI store', () => {
it('should add non-production license banner to stack based on enterprise settings', () => { it('should add non-production license banner to stack based on enterprise settings', () => {
settingsStore.setSettings( settingsStore.setSettings(
merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, { merge({}, defaultSettings, {
enterprise: { enterprise: {
showNonProdBanner: true, showNonProdBanner: true,
}, },
@ -134,7 +134,7 @@ describe('UI store', () => {
it("should add V1 banner to stack if it's not dismissed", () => { it("should add V1 banner to stack if it's not dismissed", () => {
settingsStore.setSettings( settingsStore.setSettings(
merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, { merge({}, defaultSettings, {
versionCli: '1.0.0', versionCli: '1.0.0',
}), }),
); );
@ -143,7 +143,7 @@ describe('UI store', () => {
it("should not add V1 banner to stack if it's dismissed", () => { it("should not add V1 banner to stack if it's dismissed", () => {
settingsStore.setSettings( settingsStore.setSettings(
merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, { merge({}, defaultSettings, {
versionCli: '1.0.0', versionCli: '1.0.0',
banners: { banners: {
dismissed: ['V1'], dismissed: ['V1'],

View file

@ -6,8 +6,8 @@ import { useSettingsStore } from '@/stores/settings.store';
export const useAuditLogsStore = defineStore('auditLogs', () => { export const useAuditLogsStore = defineStore('auditLogs', () => {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const isEnterpriseAuditLogsFeatureEnabled = computed(() => const isEnterpriseAuditLogsFeatureEnabled = computed(
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.AuditLogs), () => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.AuditLogs],
); );
return { return {

View file

@ -314,7 +314,7 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
projectId, projectId,
); );
if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) { if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]) {
upsertCredential(credential); upsertCredential(credential);
if (data.sharedWithProjects) { if (data.sharedWithProjects) {
await setCredentialSharedWith({ await setCredentialSharedWith({
@ -389,7 +389,7 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
sharedWithProjects: ProjectSharingData[]; sharedWithProjects: ProjectSharingData[];
credentialId: string; credentialId: string;
}): Promise<ICredentialsResponse> => { }): Promise<ICredentialsResponse> => {
if (useSettingsStore().isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) { if (useSettingsStore().isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]) {
await credentialsEeApi.setCredentialSharedWith( await credentialsEeApi.setCredentialSharedWith(
useRootStore().restApiContext, useRootStore().restApiContext,
payload.credentialId, payload.credentialId,

View file

@ -19,8 +19,8 @@ export const useExternalSecretsStore = defineStore('externalSecrets', () => {
connectionState: {} as Record<string, ExternalSecretsProvider['state']>, connectionState: {} as Record<string, ExternalSecretsProvider['state']>,
}); });
const isEnterpriseExternalSecretsEnabled = computed(() => const isEnterpriseExternalSecretsEnabled = computed(
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.ExternalSecrets), () => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.ExternalSecrets],
); );
const secrets = computed(() => state.secrets); const secrets = computed(() => state.secrets);

View file

@ -1,27 +1,15 @@
import { createApiKey, deleteApiKey, getApiKey } from '@/api/api-keys'; import * as publicApiApi from '@/api/api-keys';
import { import * as ldapApi from '@/api/ldap';
getLdapConfig, import * as settingsApi from '@/api/settings';
getLdapSynchronizations,
runLdapSync,
testLdapConnection,
updateLdapConfig,
} from '@/api/ldap';
import { getSettings, submitContactInfo } from '@/api/settings';
import { testHealthEndpoint } from '@/api/templates'; import { testHealthEndpoint } from '@/api/templates';
import type { import type { ILdapConfig } from '@/Interface';
EnterpriseEditionFeatureValue,
ILdapConfig,
IN8nPromptResponse,
ISettingsState,
} from '@/Interface';
import { STORES, INSECURE_CONNECTION_WARNING } from '@/constants'; import { STORES, INSECURE_CONNECTION_WARNING } from '@/constants';
import { UserManagementAuthenticationMethod } from '@/Interface'; import { UserManagementAuthenticationMethod } from '@/Interface';
import type { import type {
IDataObject, IDataObject,
LogLevel,
IN8nUISettings, IN8nUISettings,
ITelemetrySettings,
WorkflowSettings, WorkflowSettings,
IUserManagementSettings,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { ExpressionEvaluatorProxy } from 'n8n-workflow'; import { ExpressionEvaluatorProxy } from 'n8n-workflow';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
@ -33,382 +21,422 @@ import { makeRestApiRequest } from '@/utils/apiUtils';
import { useTitleChange } from '@/composables/useTitleChange'; import { useTitleChange } from '@/composables/useTitleChange';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { i18n } from '@/plugins/i18n'; import { i18n } from '@/plugins/i18n';
import { computed, ref } from 'vue';
export const useSettingsStore = defineStore(STORES.SETTINGS, { export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
state: (): ISettingsState => ({ const initialized = ref(false);
initialized: false, const settings = ref<IN8nUISettings>({} as IN8nUISettings);
settings: {} as IN8nUISettings, const userManagement = ref<IUserManagementSettings>({
userManagement: { quota: -1,
quota: -1, showSetupOnFirstLoad: false,
showSetupOnFirstLoad: false, smtpSetup: false,
smtpSetup: false, authenticationMethod: UserManagementAuthenticationMethod.Email,
authenticationMethod: UserManagementAuthenticationMethod.Email, });
}, const templatesEndpointHealthy = ref(false);
templatesEndpointHealthy: false, const api = ref({
api: { enabled: false,
latestVersion: 0,
path: '/',
swaggerUi: {
enabled: false, enabled: false,
latestVersion: 0, },
path: '/', });
swaggerUi: { const ldap = ref({ loginLabel: '', loginEnabled: false });
const saml = ref({ loginLabel: '', loginEnabled: false });
const mfa = ref({ enabled: false });
const saveDataErrorExecution = ref<WorkflowSettings.SaveDataExecution>('all');
const saveDataSuccessExecution = ref<WorkflowSettings.SaveDataExecution>('all');
const saveManualExecutions = ref(false);
const saveDataProgressExecution = ref(false);
const isDocker = computed(() => settings.value?.isDocker ?? false);
const databaseType = computed(() => settings.value?.databaseType);
const planName = computed(() => settings.value?.license.planName ?? 'Community');
const consumerId = computed(() => settings.value?.license.consumerId);
const binaryDataMode = computed(() => settings.value?.binaryDataMode);
const pruning = computed(() => settings.value?.pruning);
const security = computed(() => ({
blockFileAccessToN8nFiles: settings.value.security.blockFileAccessToN8nFiles,
secureCookie: settings.value.authCookie.secure,
}));
const isEnterpriseFeatureEnabled = computed(() => settings.value.enterprise);
const nodeJsVersion = computed(() => settings.value.nodeJsVersion);
const concurrency = computed(() => settings.value.concurrency);
const isPublicApiEnabled = computed(() => api.value.enabled);
const isSwaggerUIEnabled = computed(() => api.value.swaggerUi.enabled);
const isPreviewMode = computed(() => settings.value.previewMode);
const publicApiLatestVersion = computed(() => api.value.latestVersion);
const publicApiPath = computed(() => api.value.path);
const isLdapLoginEnabled = computed(() => ldap.value.loginEnabled);
const ldapLoginLabel = computed(() => ldap.value.loginLabel);
const isSamlLoginEnabled = computed(() => saml.value.loginEnabled);
const showSetupPage = computed(() => userManagement.value.showSetupOnFirstLoad);
const deploymentType = computed(() => settings.value.deployment?.type || 'default');
const isDesktopDeployment = computed(() =>
settings.value.deployment?.type.startsWith('desktop_'),
);
const isCloudDeployment = computed(() => settings.value.deployment?.type === 'cloud');
const isSmtpSetup = computed(() => userManagement.value.smtpSetup);
const isPersonalizationSurveyEnabled = computed(
() => settings.value.telemetry?.enabled && settings.value.personalizationSurveyEnabled,
);
const telemetry = computed(() => settings.value.telemetry);
const logLevel = computed(() => settings.value.logLevel);
const isTelemetryEnabled = computed(
() => settings.value.telemetry && settings.value.telemetry.enabled,
);
const isMfaFeatureEnabled = computed(() => mfa.value.enabled);
const areTagsEnabled = computed(() =>
settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true,
);
const isHiringBannerEnabled = computed(() => settings.value.hiringBannerEnabled);
const isTemplatesEnabled = computed(() =>
Boolean(settings.value.templates && settings.value.templates.enabled),
);
const isTemplatesEndpointReachable = computed(() => templatesEndpointHealthy.value);
const templatesHost = computed(() => settings.value.templates.host);
const pushBackend = computed(() => settings.value.pushBackend);
const isCommunityNodesFeatureEnabled = computed(() => settings.value.communityNodesEnabled);
const isNpmAvailable = computed(() => settings.value.isNpmAvailable);
const allowedModules = computed(() => settings.value.allowedModules);
const isQueueModeEnabled = computed(() => settings.value.executionMode === 'queue');
const isWorkerViewAvailable = computed(() => !!settings.value.enterprise?.workerView);
const workflowCallerPolicyDefaultOption = computed(
() => settings.value.workflowCallerPolicyDefaultOption,
);
const isDefaultAuthenticationSaml = computed(
() => userManagement.value.authenticationMethod === UserManagementAuthenticationMethod.Saml,
);
const permanentlyDismissedBanners = computed(() => settings.value.banners?.dismissed ?? []);
const isBelowUserQuota = computed(
() =>
userManagement.value.quota === -1 ||
userManagement.value.quota > useUsersStore().allUsers.length,
);
const isCommunityPlan = computed(() => planName.value.toLowerCase() === 'community');
const isDevRelease = computed(() => settings.value.releaseChannel === 'dev');
const setSettings = (newSettings: IN8nUISettings) => {
settings.value = newSettings;
userManagement.value = newSettings.userManagement;
if (userManagement.value) {
userManagement.value.showSetupOnFirstLoad =
!!settings.value.userManagement.showSetupOnFirstLoad;
}
api.value = settings.value.publicApi;
if (settings.value.sso?.ldap) {
ldap.value.loginEnabled = settings.value.sso.ldap.loginEnabled;
ldap.value.loginLabel = settings.value.sso.ldap.loginLabel;
}
if (settings.value.sso?.saml) {
saml.value.loginEnabled = settings.value.sso.saml.loginEnabled;
saml.value.loginLabel = settings.value.sso.saml.loginLabel;
}
mfa.value.enabled = settings.value.mfa?.enabled;
if (settings.value.enterprise?.showNonProdBanner) {
useUIStore().pushBannerToStack('NON_PRODUCTION_LICENSE');
}
if (settings.value.versionCli) {
useRootStore().setVersionCli(settings.value.versionCli);
}
if (
settings.value.authCookie.secure &&
location.protocol === 'http:' &&
!['localhost', '127.0.0.1'].includes(location.hostname)
) {
document.write(INSECURE_CONNECTION_WARNING);
return;
}
const isV1BannerDismissedPermanently = (settings.value.banners?.dismissed || []).includes('V1');
if (!isV1BannerDismissedPermanently && settings.value.versionCli.startsWith('1.')) {
useUIStore().pushBannerToStack('V1');
}
};
const setAllowedModules = (allowedModules: IN8nUISettings['allowedModules']) => {
settings.value.allowedModules = allowedModules;
};
const setSaveDataErrorExecution = (newValue: WorkflowSettings.SaveDataExecution) => {
saveDataErrorExecution.value = newValue;
};
const setSaveDataSuccessExecution = (newValue: WorkflowSettings.SaveDataExecution) => {
saveDataSuccessExecution.value = newValue;
};
const setSaveManualExecutions = (newValue: boolean) => {
saveManualExecutions.value = newValue;
};
const setSaveDataProgressExecution = (newValue: boolean) => {
saveDataProgressExecution.value = newValue;
};
const getSettings = async () => {
const rootStore = useRootStore();
const fetchedSettings = await settingsApi.getSettings(rootStore.restApiContext);
setSettings(fetchedSettings);
settings.value.communityNodesEnabled = fetchedSettings.communityNodesEnabled;
setAllowedModules(fetchedSettings.allowedModules);
setSaveDataErrorExecution(fetchedSettings.saveDataErrorExecution);
setSaveDataSuccessExecution(fetchedSettings.saveDataSuccessExecution);
setSaveDataProgressExecution(fetchedSettings.saveExecutionProgress);
setSaveManualExecutions(fetchedSettings.saveManualExecutions);
rootStore.setUrlBaseWebhook(fetchedSettings.urlBaseWebhook);
rootStore.setUrlBaseEditor(fetchedSettings.urlBaseEditor);
rootStore.setEndpointForm(fetchedSettings.endpointForm);
rootStore.setEndpointFormTest(fetchedSettings.endpointFormTest);
rootStore.setEndpointFormWaiting(fetchedSettings.endpointFormWaiting);
rootStore.setEndpointWebhook(fetchedSettings.endpointWebhook);
rootStore.setEndpointWebhookTest(fetchedSettings.endpointWebhookTest);
rootStore.setTimezone(fetchedSettings.timezone);
rootStore.setExecutionTimeout(fetchedSettings.executionTimeout);
rootStore.setMaxExecutionTimeout(fetchedSettings.maxExecutionTimeout);
rootStore.setInstanceId(fetchedSettings.instanceId);
rootStore.setOauthCallbackUrls(fetchedSettings.oauthCallbackUrls);
rootStore.setN8nMetadata(fetchedSettings.n8nMetadata || {});
rootStore.setDefaultLocale(fetchedSettings.defaultLocale);
rootStore.setIsNpmAvailable(fetchedSettings.isNpmAvailable);
rootStore.setBinaryDataMode(fetchedSettings.binaryDataMode);
useVersionsStore().setVersionNotificationSettings(fetchedSettings.versionNotifications);
};
const initialize = async () => {
if (initialized.value) {
return;
}
const { showToast } = useToast();
try {
await getSettings();
ExpressionEvaluatorProxy.setEvaluator(settings.value.expressions.evaluator);
// Re-compute title since settings are now available
useTitleChange().titleReset();
initialized.value = true;
} catch (e) {
showToast({
title: i18n.baseText('startupError'),
message: i18n.baseText('startupError.message'),
type: 'error',
duration: 0,
dangerouslyUseHTMLString: true,
});
throw e;
}
};
const stopShowingSetupPage = () => {
userManagement.value.showSetupOnFirstLoad = false;
};
const disableTemplates = () => {
settings.value = {
...settings.value,
templates: {
...settings.value.templates,
enabled: false, enabled: false,
}, },
}, };
ldap: { };
loginLabel: '',
loginEnabled: false,
},
saml: {
loginLabel: '',
loginEnabled: false,
},
mfa: {
enabled: false,
},
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
saveManualExecutions: false,
saveDataProgressExecution: false,
}),
getters: {
isDocker(): boolean {
return this.settings.isDocker;
},
databaseType(): 'sqlite' | 'mariadb' | 'mysqldb' | 'postgresdb' {
return this.settings.databaseType;
},
planName(): string {
return this.settings.license?.planName ?? 'Community';
},
isCommunityPlan(): boolean {
return this.planName.toLowerCase() === 'community';
},
consumerId(): string {
return this.settings.license?.consumerId ?? 'unknown';
},
binaryDataMode(): 'default' | 'filesystem' | 's3' {
return this.settings.binaryDataMode;
},
pruning(): { isEnabled: boolean; maxAge: number; maxCount: number } {
return this.settings.pruning;
},
security(): {
blockFileAccessToN8nFiles: boolean;
secureCookie: boolean;
} {
return {
blockFileAccessToN8nFiles: this.settings.security.blockFileAccessToN8nFiles,
secureCookie: this.settings.authCookie.secure,
};
},
isEnterpriseFeatureEnabled() {
return (feature: EnterpriseEditionFeatureValue): boolean =>
Boolean(this.settings.enterprise?.[feature]);
},
versionCli(): string { const submitContactInfo = async (email: string) => {
return this.settings.versionCli; try {
}, const usersStore = useUsersStore();
nodeJsVersion(): string { return await settingsApi.submitContactInfo(
return this.settings.nodeJsVersion; settings.value.instanceId,
}, usersStore.currentUserId || '',
concurrency(): number { email,
return this.settings.concurrency;
},
isPublicApiEnabled(): boolean {
return this.api.enabled;
},
isSwaggerUIEnabled(): boolean {
return this.api.swaggerUi.enabled;
},
isPreviewMode(): boolean {
return this.settings.previewMode;
},
publicApiLatestVersion(): number {
return this.api.latestVersion;
},
publicApiPath(): string {
return this.api.path;
},
isLdapLoginEnabled(): boolean {
return this.ldap.loginEnabled;
},
ldapLoginLabel(): string {
return this.ldap.loginLabel;
},
isSamlLoginEnabled(): boolean {
return this.saml.loginEnabled;
},
samlLoginLabel(): string {
return this.saml.loginLabel;
},
showSetupPage(): boolean {
return this.userManagement.showSetupOnFirstLoad === true;
},
deploymentType(): string {
return this.settings.deployment?.type || 'default';
},
isDesktopDeployment(): boolean {
if (!this.settings.deployment) {
return false;
}
return this.settings.deployment?.type.startsWith('desktop_');
},
isCloudDeployment(): boolean {
return this.settings.deployment?.type === 'cloud';
},
isSmtpSetup(): boolean {
return this.userManagement.smtpSetup;
},
isPersonalizationSurveyEnabled(): boolean {
return (
this.settings.telemetry &&
this.settings.telemetry.enabled &&
this.settings.personalizationSurveyEnabled
); );
}, } catch (error) {
telemetry(): ITelemetrySettings { return;
return this.settings.telemetry; }
}, };
logLevel(): LogLevel {
return this.settings.logLevel;
},
isTelemetryEnabled(): boolean {
return this.settings.telemetry && this.settings.telemetry.enabled;
},
isMfaFeatureEnabled(): boolean {
return this.settings?.mfa?.enabled;
},
areTagsEnabled(): boolean {
return this.settings.workflowTagsDisabled !== undefined
? !this.settings.workflowTagsDisabled
: true;
},
isHiringBannerEnabled(): boolean {
return this.settings.hiringBannerEnabled;
},
isTemplatesEnabled(): boolean {
return Boolean(this.settings.templates && this.settings.templates.enabled);
},
isTemplatesEndpointReachable(): boolean {
return this.templatesEndpointHealthy;
},
templatesHost(): string {
return this.settings.templates.host;
},
pushBackend(): IN8nUISettings['pushBackend'] {
return this.settings.pushBackend;
},
isCommunityNodesFeatureEnabled(): boolean {
return this.settings.communityNodesEnabled;
},
isNpmAvailable(): boolean {
return this.settings.isNpmAvailable;
},
allowedModules(): { builtIn?: string[]; external?: string[] } {
return this.settings.allowedModules;
},
isQueueModeEnabled(): boolean {
return this.settings.executionMode === 'queue';
},
isWorkerViewAvailable(): boolean {
return !!this.settings.enterprise?.workerView;
},
workflowCallerPolicyDefaultOption(): WorkflowSettings.CallerPolicy {
return this.settings.workflowCallerPolicyDefaultOption;
},
isDefaultAuthenticationSaml(): boolean {
return this.userManagement.authenticationMethod === UserManagementAuthenticationMethod.Saml;
},
permanentlyDismissedBanners(): string[] {
return this.settings.banners?.dismissed ?? [];
},
isBelowUserQuota(): boolean {
const userStore = useUsersStore();
return (
this.userManagement.quota === -1 || this.userManagement.quota > userStore.allUsers.length
);
},
isDevRelease(): boolean {
return this.settings.releaseChannel === 'dev';
},
},
actions: {
async initialize() {
if (this.initialized) {
return;
}
const { showToast } = useToast(); const testTemplatesEndpoint = async () => {
try { const timeout = new Promise((_, reject) => setTimeout(() => reject(), 2000));
await this.getSettings(); await Promise.race([testHealthEndpoint(templatesHost.value), timeout]);
templatesEndpointHealthy.value = true;
};
ExpressionEvaluatorProxy.setEvaluator(this.settings.expressions.evaluator); const getApiKey = async () => {
const rootStore = useRootStore();
const { apiKey } = await publicApiApi.getApiKey(rootStore.restApiContext);
return apiKey;
};
// Re-compute title since settings are now available const createApiKey = async () => {
useTitleChange().titleReset(); const rootStore = useRootStore();
const { apiKey } = await publicApiApi.createApiKey(rootStore.restApiContext);
return apiKey;
};
this.initialized = true; const deleteApiKey = async () => {
} catch (e) { const rootStore = useRootStore();
showToast({ await publicApiApi.deleteApiKey(rootStore.restApiContext);
title: i18n.baseText('startupError'), };
message: i18n.baseText('startupError.message'),
type: 'error',
duration: 0,
dangerouslyUseHTMLString: true,
});
throw e; const getLdapConfig = async () => {
} const rootStore = useRootStore();
}, return await ldapApi.getLdapConfig(rootStore.restApiContext);
setSettings(settings: IN8nUISettings): void { };
this.settings = settings;
this.userManagement = settings.userManagement;
if (this.userManagement) {
this.userManagement.showSetupOnFirstLoad = !!settings.userManagement.showSetupOnFirstLoad;
}
this.api = settings.publicApi;
if (settings.sso?.ldap) {
this.ldap.loginEnabled = settings.sso.ldap.loginEnabled;
this.ldap.loginLabel = settings.sso.ldap.loginLabel;
}
if (settings.sso?.saml) {
this.saml.loginEnabled = settings.sso.saml.loginEnabled;
this.saml.loginLabel = settings.sso.saml.loginLabel;
}
if (settings.enterprise?.showNonProdBanner) {
useUIStore().pushBannerToStack('NON_PRODUCTION_LICENSE');
}
if (settings.versionCli) {
useRootStore().setVersionCli(settings.versionCli);
}
if ( const getLdapSynchronizations = async (pagination: { page: number }) => {
settings.authCookie.secure && const rootStore = useRootStore();
location.protocol === 'http:' && return await ldapApi.getLdapSynchronizations(rootStore.restApiContext, pagination);
!['localhost', '127.0.0.1'].includes(location.hostname) };
) {
document.write(INSECURE_CONNECTION_WARNING);
return;
}
const isV1BannerDismissedPermanently = (settings.banners?.dismissed || []).includes('V1'); const testLdapConnection = async () => {
if (!isV1BannerDismissedPermanently && useRootStore().versionCli.startsWith('1.')) { const rootStore = useRootStore();
useUIStore().pushBannerToStack('V1'); return await ldapApi.testLdapConnection(rootStore.restApiContext);
} };
},
async getSettings(): Promise<void> {
const rootStore = useRootStore();
const settings = await getSettings(rootStore.restApiContext);
this.setSettings(settings); const updateLdapConfig = async (ldapConfig: ILdapConfig) => {
this.settings.communityNodesEnabled = settings.communityNodesEnabled; const rootStore = useRootStore();
this.setAllowedModules(settings.allowedModules); return await ldapApi.updateLdapConfig(rootStore.restApiContext, ldapConfig);
this.setSaveDataErrorExecution(settings.saveDataErrorExecution); };
this.setSaveDataSuccessExecution(settings.saveDataSuccessExecution);
this.setSaveDataProgressExecution(settings.saveExecutionProgress);
this.setSaveManualExecutions(settings.saveManualExecutions);
rootStore.setUrlBaseWebhook(settings.urlBaseWebhook); const runLdapSync = async (data: IDataObject) => {
rootStore.setUrlBaseEditor(settings.urlBaseEditor); const rootStore = useRootStore();
rootStore.setEndpointForm(settings.endpointForm); return await ldapApi.runLdapSync(rootStore.restApiContext, data);
rootStore.setEndpointFormTest(settings.endpointFormTest); };
rootStore.setEndpointFormWaiting(settings.endpointFormWaiting);
rootStore.setEndpointWebhook(settings.endpointWebhook);
rootStore.setEndpointWebhookTest(settings.endpointWebhookTest);
rootStore.setTimezone(settings.timezone);
rootStore.setExecutionTimeout(settings.executionTimeout);
rootStore.setMaxExecutionTimeout(settings.maxExecutionTimeout);
rootStore.setVersionCli(settings.versionCli);
rootStore.setInstanceId(settings.instanceId);
rootStore.setOauthCallbackUrls(settings.oauthCallbackUrls);
rootStore.setN8nMetadata(settings.n8nMetadata || {});
rootStore.setDefaultLocale(settings.defaultLocale);
rootStore.setIsNpmAvailable(settings.isNpmAvailable);
rootStore.setBinaryDataMode(settings.binaryDataMode);
useVersionsStore().setVersionNotificationSettings(settings.versionNotifications); const getTimezones = async (): Promise<IDataObject> => {
}, const rootStore = useRootStore();
stopShowingSetupPage(): void { return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/options/timezones');
this.userManagement.showSetupOnFirstLoad = false; };
},
disableTemplates(): void { const reset = () => {
this.settings = { settings.value = {} as IN8nUISettings;
...this.settings, };
templates: {
...this.settings.templates, return {
enabled: false, settings,
}, userManagement,
}; templatesEndpointHealthy,
}, api,
setAllowedModules(allowedModules: { builtIn?: string[]; external?: string[] }): void { ldap,
this.settings.allowedModules = allowedModules; saml,
}, mfa,
async submitContactInfo(email: string): Promise<IN8nPromptResponse | undefined> { isDocker,
try { isDevRelease,
const usersStore = useUsersStore(); isEnterpriseFeatureEnabled,
return await submitContactInfo( databaseType,
this.settings.instanceId, planName,
usersStore.currentUserId || '', consumerId,
email, binaryDataMode,
); pruning,
} catch (error) { security,
return; nodeJsVersion,
} concurrency,
}, isPublicApiEnabled,
async testTemplatesEndpoint(): Promise<void> { isSwaggerUIEnabled,
const timeout = new Promise((_, reject) => setTimeout(() => reject(), 2000)); isPreviewMode,
await Promise.race([testHealthEndpoint(this.templatesHost), timeout]); publicApiLatestVersion,
this.templatesEndpointHealthy = true; publicApiPath,
}, isLdapLoginEnabled,
async getApiKey(): Promise<string | null> { ldapLoginLabel,
const rootStore = useRootStore(); isSamlLoginEnabled,
const { apiKey } = await getApiKey(rootStore.restApiContext); showSetupPage,
return apiKey; deploymentType,
}, isDesktopDeployment,
async createApiKey(): Promise<string | null> { isCloudDeployment,
const rootStore = useRootStore(); isSmtpSetup,
const { apiKey } = await createApiKey(rootStore.restApiContext); isPersonalizationSurveyEnabled,
return apiKey; telemetry,
}, logLevel,
async deleteApiKey(): Promise<void> { isTelemetryEnabled,
const rootStore = useRootStore(); isMfaFeatureEnabled,
await deleteApiKey(rootStore.restApiContext); areTagsEnabled,
}, isHiringBannerEnabled,
async getLdapConfig() { isTemplatesEnabled,
const rootStore = useRootStore(); isTemplatesEndpointReachable,
return await getLdapConfig(rootStore.restApiContext); templatesHost,
}, pushBackend,
async getLdapSynchronizations(pagination: { page: number }) { isCommunityNodesFeatureEnabled,
const rootStore = useRootStore(); isNpmAvailable,
return await getLdapSynchronizations(rootStore.restApiContext, pagination); allowedModules,
}, isQueueModeEnabled,
async testLdapConnection() { isWorkerViewAvailable,
const rootStore = useRootStore(); isDefaultAuthenticationSaml,
return await testLdapConnection(rootStore.restApiContext); workflowCallerPolicyDefaultOption,
}, permanentlyDismissedBanners,
async updateLdapConfig(ldapConfig: ILdapConfig) { isBelowUserQuota,
const rootStore = useRootStore(); saveDataErrorExecution,
return await updateLdapConfig(rootStore.restApiContext, ldapConfig); saveDataSuccessExecution,
}, saveManualExecutions,
async runLdapSync(data: IDataObject) { saveDataProgressExecution,
const rootStore = useRootStore(); isCommunityPlan,
return await runLdapSync(rootStore.restApiContext, data); reset,
}, testLdapConnection,
setSaveDataErrorExecution(newValue: WorkflowSettings.SaveDataExecution) { getLdapConfig,
this.saveDataErrorExecution = newValue; getLdapSynchronizations,
}, updateLdapConfig,
setSaveDataSuccessExecution(newValue: WorkflowSettings.SaveDataExecution) { runLdapSync,
this.saveDataSuccessExecution = newValue; getTimezones,
}, createApiKey,
setSaveManualExecutions(saveManualExecutions: boolean) { getApiKey,
this.saveManualExecutions = saveManualExecutions; deleteApiKey,
}, testTemplatesEndpoint,
setSaveDataProgressExecution(newValue: boolean) { submitContactInfo,
this.saveDataProgressExecution = newValue; disableTemplates,
}, stopShowingSetupPage,
async getTimezones(): Promise<IDataObject> { getSettings,
const rootStore = useRootStore(); setSettings,
return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/options/timezones'); initialize,
}, };
},
}); });

View file

@ -11,8 +11,8 @@ export const useSourceControlStore = defineStore('sourceControl', () => {
const rootStore = useRootStore(); const rootStore = useRootStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const isEnterpriseSourceControlEnabled = computed(() => const isEnterpriseSourceControlEnabled = computed(
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.SourceControl), () => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.SourceControl],
); );
const sshKeyTypes: SshKeyTypes = ['ed25519', 'rsa']; const sshKeyTypes: SshKeyTypes = ['ed25519', 'rsa'];

View file

@ -42,8 +42,8 @@ export const useSSOStore = defineStore('sso', () => {
void toggleLoginEnabled(value); void toggleLoginEnabled(value);
}, },
}); });
const isEnterpriseSamlEnabled = computed(() => const isEnterpriseSamlEnabled = computed(
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Saml), () => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Saml],
); );
const isDefaultAuthenticationSaml = computed(() => settingsStore.isDefaultAuthenticationSaml); const isDefaultAuthenticationSaml = computed(() => settingsStore.isDefaultAuthenticationSaml);
const showSsoLoginButton = computed( const showSsoLoginButton = computed(

View file

@ -289,8 +289,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
}, },
async fetchTemplateById(templateId: string): Promise<ITemplatesWorkflowFull> { async fetchTemplateById(templateId: string): Promise<ITemplatesWorkflowFull> {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost; const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = settingsStore.versionCli; const versionCli: string = rootStore.versionCli;
const response = await getTemplateById(apiEndpoint, templateId, { const response = await getTemplateById(apiEndpoint, templateId, {
'n8n-version': versionCli, 'n8n-version': versionCli,
}); });
@ -305,8 +306,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
}, },
async fetchCollectionById(collectionId: string): Promise<ITemplatesCollection | null> { async fetchCollectionById(collectionId: string): Promise<ITemplatesCollection | null> {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost; const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = settingsStore.versionCli; const versionCli: string = rootStore.versionCli;
const response = await getCollectionById(apiEndpoint, collectionId, { const response = await getCollectionById(apiEndpoint, collectionId, {
'n8n-version': versionCli, 'n8n-version': versionCli,
}); });
@ -325,8 +327,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
return cachedCategories; return cachedCategories;
} }
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost; const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = settingsStore.versionCli; const versionCli: string = rootStore.versionCli;
const response = await getCategories(apiEndpoint, { 'n8n-version': versionCli }); const response = await getCategories(apiEndpoint, { 'n8n-version': versionCli });
const categories = response.categories; const categories = response.categories;
@ -340,8 +343,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
} }
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost; const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = settingsStore.versionCli; const versionCli: string = rootStore.versionCli;
const response = await getCollections(apiEndpoint, query, { 'n8n-version': versionCli }); const response = await getCollections(apiEndpoint, query, { 'n8n-version': versionCli });
const collections = response.collections; const collections = response.collections;
@ -361,8 +365,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
} }
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost; const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = settingsStore.versionCli; const versionCli: string = rootStore.versionCli;
const payload = await getWorkflows( const payload = await getWorkflows(
apiEndpoint, apiEndpoint,
@ -402,8 +407,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
}, },
async getWorkflowTemplate(templateId: string): Promise<IWorkflowTemplate> { async getWorkflowTemplate(templateId: string): Promise<IWorkflowTemplate> {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost; const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = settingsStore.versionCli; const versionCli: string = rootStore.versionCli;
return await getWorkflowTemplate(apiEndpoint, templateId, { 'n8n-version': versionCli }); return await getWorkflowTemplate(apiEndpoint, templateId, { 'n8n-version': versionCli });
}, },

View file

@ -50,7 +50,7 @@ export const useWorkflowsEEStore = defineStore(STORES.WORKFLOWS_EE, {
const rootStore = useRootStore(); const rootStore = useRootStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) { if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]) {
await setWorkflowSharedWith(rootStore.restApiContext, payload.workflowId, { await setWorkflowSharedWith(rootStore.restApiContext, payload.workflowId, {
shareWithIds: payload.sharedWithProjects.map((p) => p.id), shareWithIds: payload.sharedWithProjects.map((p) => p.id),
}); });

View file

@ -1,23 +1,24 @@
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { isEnterpriseFeatureEnabled } from '@/utils/rbac/checks/isEnterpriseFeatureEnabled'; import { isEnterpriseFeatureEnabled } from '@/utils/rbac/checks/isEnterpriseFeatureEnabled';
import { EnterpriseEditionFeature } from '@/constants'; import { EnterpriseEditionFeature } from '@/constants';
import { createPinia, setActivePinia } from 'pinia';
vi.mock('@/stores/settings.store', () => ({ import { defaultSettings } from '@/__tests__/defaults';
useSettingsStore: vi.fn(),
}));
describe('Checks', () => { describe('Checks', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
describe('isEnterpriseFeatureEnabled()', () => { describe('isEnterpriseFeatureEnabled()', () => {
it('should return true if no feature is provided', () => { it('should return true if no feature is provided', () => {
expect(isEnterpriseFeatureEnabled({})).toBe(true); expect(isEnterpriseFeatureEnabled({})).toBe(true);
}); });
it('should return true if feature is enabled', () => { it('should return true if feature is enabled', () => {
vi.mocked(useSettingsStore).mockReturnValue({ useSettingsStore().settings.enterprise = {
isEnterpriseFeatureEnabled: vi ...defaultSettings.enterprise,
.fn() [EnterpriseEditionFeature.Saml]: true,
.mockImplementation((feature) => feature !== EnterpriseEditionFeature.Variables), };
} as unknown as ReturnType<typeof useSettingsStore>);
expect( expect(
isEnterpriseFeatureEnabled({ isEnterpriseFeatureEnabled({
@ -27,11 +28,11 @@ describe('Checks', () => {
}); });
it('should return true if all features are enabled in allOf mode', () => { it('should return true if all features are enabled in allOf mode', () => {
vi.mocked(useSettingsStore).mockReturnValue({ useSettingsStore().settings.enterprise = {
isEnterpriseFeatureEnabled: vi ...defaultSettings.enterprise,
.fn() [EnterpriseEditionFeature.Ldap]: true,
.mockImplementation((feature) => feature !== EnterpriseEditionFeature.Variables), [EnterpriseEditionFeature.Saml]: true,
} as unknown as ReturnType<typeof useSettingsStore>); };
expect( expect(
isEnterpriseFeatureEnabled({ isEnterpriseFeatureEnabled({
@ -42,11 +43,11 @@ describe('Checks', () => {
}); });
it('should return false if any feature is not enabled in allOf mode', () => { it('should return false if any feature is not enabled in allOf mode', () => {
vi.mocked(useSettingsStore).mockReturnValue({ useSettingsStore().settings.enterprise = {
isEnterpriseFeatureEnabled: vi ...defaultSettings.enterprise,
.fn() [EnterpriseEditionFeature.Ldap]: true,
.mockImplementation((feature) => feature !== EnterpriseEditionFeature.Saml), [EnterpriseEditionFeature.Saml]: false,
} as unknown as ReturnType<typeof useSettingsStore>); };
expect( expect(
isEnterpriseFeatureEnabled({ isEnterpriseFeatureEnabled({
@ -57,11 +58,11 @@ describe('Checks', () => {
}); });
it('should return true if any feature is enabled in oneOf mode', () => { it('should return true if any feature is enabled in oneOf mode', () => {
vi.mocked(useSettingsStore).mockReturnValue({ useSettingsStore().settings.enterprise = {
isEnterpriseFeatureEnabled: vi ...defaultSettings.enterprise,
.fn() [EnterpriseEditionFeature.Ldap]: true,
.mockImplementation((feature) => feature === EnterpriseEditionFeature.Ldap), [EnterpriseEditionFeature.Saml]: false,
} as unknown as ReturnType<typeof useSettingsStore>); };
expect( expect(
isEnterpriseFeatureEnabled({ isEnterpriseFeatureEnabled({
@ -72,9 +73,11 @@ describe('Checks', () => {
}); });
it('should return false if no features are enabled in anyOf mode', () => { it('should return false if no features are enabled in anyOf mode', () => {
vi.mocked(useSettingsStore).mockReturnValue({ useSettingsStore().settings.enterprise = {
isEnterpriseFeatureEnabled: vi.fn().mockReturnValue(false), ...defaultSettings.enterprise,
} as unknown as ReturnType<typeof useSettingsStore>); [EnterpriseEditionFeature.Ldap]: false,
[EnterpriseEditionFeature.Saml]: false,
};
expect( expect(
isEnterpriseFeatureEnabled({ isEnterpriseFeatureEnabled({

View file

@ -12,8 +12,8 @@ export const isEnterpriseFeatureEnabled: RBACPermissionCheck<EnterprisePermissio
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const mode = options.mode ?? 'allOf'; const mode = options.mode ?? 'allOf';
if (mode === 'allOf') { if (mode === 'allOf') {
return features.every(settingsStore.isEnterpriseFeatureEnabled); return features.every((feature) => settingsStore.isEnterpriseFeatureEnabled[feature]);
} else { } else {
return features.some(settingsStore.isEnterpriseFeatureEnabled); return features.some((feature) => settingsStore.isEnterpriseFeatureEnabled[feature]);
} }
}; };

View file

@ -3,17 +3,21 @@ import { VIEWS, EnterpriseEditionFeature } from '@/constants';
import { enterpriseMiddleware } from '@/utils/rbac/middleware/enterprise'; import { enterpriseMiddleware } from '@/utils/rbac/middleware/enterprise';
import { type RouteLocationNormalized } from 'vue-router'; import { type RouteLocationNormalized } from 'vue-router';
import type { EnterprisePermissionOptions } from '@/types/rbac'; import type { EnterprisePermissionOptions } from '@/types/rbac';
import { createPinia, setActivePinia } from 'pinia';
vi.mock('@/stores/settings.store', () => ({ import { defaultSettings } from '@/__tests__/defaults';
useSettingsStore: vi.fn(),
}));
describe('Middleware', () => { describe('Middleware', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
describe('enterprise', () => { describe('enterprise', () => {
it('should redirect to homepage if none of the required features are enabled in allOf mode', async () => { it('should redirect to homepage if none of the required features are enabled in allOf mode', async () => {
vi.mocked(useSettingsStore).mockReturnValue({ useSettingsStore().settings.enterprise = {
isEnterpriseFeatureEnabled: (_) => false, ...defaultSettings.enterprise,
} as ReturnType<typeof useSettingsStore>); [EnterpriseEditionFeature.Ldap]: false,
[EnterpriseEditionFeature.Saml]: false,
};
const nextMock = vi.fn(); const nextMock = vi.fn();
const options: EnterprisePermissionOptions = { const options: EnterprisePermissionOptions = {
@ -32,10 +36,11 @@ describe('Middleware', () => {
}); });
it('should allow navigation if all of the required features are enabled in allOf mode', async () => { it('should allow navigation if all of the required features are enabled in allOf mode', async () => {
vi.mocked(useSettingsStore).mockReturnValue({ useSettingsStore().settings.enterprise = {
isEnterpriseFeatureEnabled: (feature) => ...defaultSettings.enterprise,
[EnterpriseEditionFeature.Saml, EnterpriseEditionFeature.Ldap].includes(feature), [EnterpriseEditionFeature.Ldap]: true,
} as ReturnType<typeof useSettingsStore>); [EnterpriseEditionFeature.Saml]: true,
};
const nextMock = vi.fn(); const nextMock = vi.fn();
const options: EnterprisePermissionOptions = { const options: EnterprisePermissionOptions = {
@ -54,9 +59,10 @@ describe('Middleware', () => {
}); });
it('should redirect to homepage if none of the required features are enabled in oneOf mode', async () => { it('should redirect to homepage if none of the required features are enabled in oneOf mode', async () => {
vi.mocked(useSettingsStore).mockReturnValue({ useSettingsStore().settings.enterprise = {
isEnterpriseFeatureEnabled: (_) => false, ...defaultSettings.enterprise,
} as ReturnType<typeof useSettingsStore>); [EnterpriseEditionFeature.Saml]: false,
};
const nextMock = vi.fn(); const nextMock = vi.fn();
const options: EnterprisePermissionOptions = { const options: EnterprisePermissionOptions = {
@ -75,9 +81,11 @@ describe('Middleware', () => {
}); });
it('should allow navigation if at least one of the required features is enabled in oneOf mode', async () => { it('should allow navigation if at least one of the required features is enabled in oneOf mode', async () => {
vi.mocked(useSettingsStore).mockReturnValue({ useSettingsStore().settings.enterprise = {
isEnterpriseFeatureEnabled: (feature) => feature === EnterpriseEditionFeature.Saml, ...defaultSettings.enterprise,
} as ReturnType<typeof useSettingsStore>); [EnterpriseEditionFeature.Ldap]: true,
[EnterpriseEditionFeature.Saml]: false,
};
const nextMock = vi.fn(); const nextMock = vi.fn();
const options: EnterprisePermissionOptions = { const options: EnterprisePermissionOptions = {

View file

@ -159,9 +159,8 @@ export default defineComponent({
}, },
async initialize() { async initialize() {
this.loading = true; this.loading = true;
const isVarsEnabled = useSettingsStore().isEnterpriseFeatureEnabled( const isVarsEnabled =
EnterpriseEditionFeature.Variables, useSettingsStore().isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Variables];
);
const loadPromises = [ const loadPromises = [
this.credentialsStore.fetchAllCredentials( this.credentialsStore.fetchAllCredentials(

View file

@ -210,11 +210,11 @@ async function initializeData() {
credentialsStore.fetchCredentialTypes(true), credentialsStore.fetchCredentialTypes(true),
]; ];
if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Variables)) { if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Variables]) {
promises.push(environmentsStore.fetchAllVariables()); promises.push(environmentsStore.fetchAllVariables());
} }
if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.ExternalSecrets)) { if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.ExternalSecrets]) {
promises.push(externalSecretsStore.fetchAllSecrets()); promises.push(externalSecretsStore.fetchAllSecrets());
} }

View file

@ -833,10 +833,10 @@ export default defineComponent({
const loadPromises = (() => { const loadPromises = (() => {
if (this.settingsStore.isPreviewMode && this.isDemo) return []; if (this.settingsStore.isPreviewMode && this.isDemo) return [];
const promises = [this.loadActiveWorkflows(), this.loadCredentialTypes()]; const promises = [this.loadActiveWorkflows(), this.loadCredentialTypes()];
if (this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Variables)) { if (this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Variables]) {
promises.push(this.loadVariables()); promises.push(this.loadVariables());
} }
if (this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.ExternalSecrets)) { if (this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.ExternalSecrets]) {
promises.push(this.loadSecrets()); promises.push(this.loadSecrets());
} }
return promises; return promises;
@ -4208,7 +4208,7 @@ export default defineComponent({
if ( if (
nodeData.credentials && nodeData.credentials &&
this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing) this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]
) { ) {
const usedCredentials = this.workflowsStore.usedCredentials; const usedCredentials = this.workflowsStore.usedCredentials;
nodeData.credentials = Object.fromEntries( nodeData.credentials = Object.fromEntries(

View file

@ -153,7 +153,7 @@ export default defineComponent({
}, },
isLicensed(): boolean { isLicensed(): boolean {
if (this.disableLicense) return false; if (this.disableLicense) return false;
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.LogStreaming); return this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.LogStreaming];
}, },
canManageLogStreaming(): boolean { canManageLogStreaming(): boolean {
return hasPermission(['rbac'], { rbac: { scope: 'logStreaming:manage' } }); return hasPermission(['rbac'], { rbac: { scope: 'logStreaming:manage' } });

View file

@ -76,7 +76,7 @@ const usersListActions = computed((): IUserListAction[] => {
]; ];
}); });
const isAdvancedPermissionsEnabled = computed((): boolean => { const isAdvancedPermissionsEnabled = computed((): boolean => {
return settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.AdvancedPermissions); return settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.AdvancedPermissions];
}); });
const userRoles = computed((): Array<{ value: IRole; label: string; disabled?: boolean }> => { const userRoles = computed((): Array<{ value: IRole; label: string; disabled?: boolean }> => {

View file

@ -7,7 +7,9 @@ import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { useRBACStore } from '@/stores/rbac.store'; import { useRBACStore } from '@/stores/rbac.store';
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import { EnterpriseEditionFeature } from '@/constants'; import { EnterpriseEditionFeature, STORES } from '@/constants';
import { createTestingPinia } from '@pinia/testing';
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
describe('VariablesView', () => { describe('VariablesView', () => {
let server: ReturnType<typeof setupServer>; let server: ReturnType<typeof setupServer>;
@ -16,7 +18,13 @@ describe('VariablesView', () => {
let usersStore: ReturnType<typeof useUsersStore>; let usersStore: ReturnType<typeof useUsersStore>;
let rbacStore: ReturnType<typeof useRBACStore>; let rbacStore: ReturnType<typeof useRBACStore>;
const renderComponent = createComponentRenderer(VariablesView); const renderComponent = createComponentRenderer(VariablesView, {
pinia: createTestingPinia({
initialState: {
[STORES.SETTINGS]: SETTINGS_STORE_DEFAULT_STATE,
},
}),
});
beforeAll(() => { beforeAll(() => {
server = setupServer(); server = setupServer();
@ -105,6 +113,7 @@ describe('VariablesView', () => {
}); });
it('should render variable entries', async () => { it('should render variable entries', async () => {
settingsStore.settings.enterprise[EnterpriseEditionFeature.Variables] = true;
server.createList('variable', 3); server.createList('variable', 3);
const wrapper = renderComponent({ pinia }); const wrapper = renderComponent({ pinia });

View file

@ -41,8 +41,8 @@ const editMode = ref<Record<string, boolean>>({});
const loading = ref(false); const loading = ref(false);
const permissions = getVariablesPermissions(usersStore.currentUser); const permissions = getVariablesPermissions(usersStore.currentUser);
const isFeatureEnabled = computed(() => const isFeatureEnabled = computed(
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Variables), () => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Variables],
); );
const variablesToResources = computed((): IResource[] => const variablesToResources = computed((): IResource[] =>

View file

@ -217,7 +217,7 @@ const WorkflowsView = defineComponent({
return this.workflowsStore.allWorkflows; return this.workflowsStore.allWorkflows;
}, },
isShareable(): boolean { isShareable(): boolean {
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing); return this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing];
}, },
statusFilterOptions(): Array<{ label: string; value: string | boolean }> { statusFilterOptions(): Array<{ label: string; value: string | boolean }> {
return [ return [

View file

@ -11,8 +11,10 @@ import { useUsersStore } from '@/stores/users.store';
import { createUser } from '@/__tests__/data/users'; import { createUser } from '@/__tests__/data/users';
import { createProjectListItem } from '@/__tests__/data/projects'; import { createProjectListItem } from '@/__tests__/data/projects';
import { useRBACStore } from '@/stores/rbac.store'; import { useRBACStore } from '@/stores/rbac.store';
import { DELETE_USER_MODAL_KEY } from '@/constants'; import { DELETE_USER_MODAL_KEY, EnterpriseEditionFeature } from '@/constants';
import * as usersApi from '@/api/users'; import * as usersApi from '@/api/users';
import { useSettingsStore } from '@/stores/settings.store';
import { defaultSettings } from '@/__tests__/defaults';
const wrapperComponentWithModal = { const wrapperComponentWithModal = {
components: { SettingsUsersView, ModalRoot, DeleteUserModal }, components: { SettingsUsersView, ModalRoot, DeleteUserModal },
@ -47,6 +49,11 @@ describe('SettingsUsersView', () => {
usersStore = useUsersStore(); usersStore = useUsersStore();
rbacStore = useRBACStore(); rbacStore = useRBACStore();
useSettingsStore().settings.enterprise = {
...defaultSettings.enterprise,
[EnterpriseEditionFeature.AdvancedExecutionFilters]: true,
};
vi.spyOn(rbacStore, 'hasScope').mockReturnValue(true); vi.spyOn(rbacStore, 'hasScope').mockReturnValue(true);
vi.spyOn(usersApi, 'getUsers').mockResolvedValue(users); vi.spyOn(usersApi, 'getUsers').mockResolvedValue(users);
vi.spyOn(usersStore, 'allUsers', 'get').mockReturnValue(users); vi.spyOn(usersStore, 'allUsers', 'get').mockReturnValue(users);

View file

@ -2596,7 +2596,7 @@ export type ExpressionEvaluatorType = 'tmpl' | 'tournament';
export type N8nAIProviderType = 'openai' | 'unknown'; export type N8nAIProviderType = 'openai' | 'unknown';
export interface IN8nUISettings { export interface IN8nUISettings {
isDocker: boolean; isDocker?: boolean;
databaseType: 'sqlite' | 'mariadb' | 'mysqldb' | 'postgresdb'; databaseType: 'sqlite' | 'mariadb' | 'mysqldb' | 'postgresdb';
endpointForm: string; endpointForm: string;
endpointFormTest: string; endpointFormTest: string;