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,
debugInEditor: false,
advancedExecutionFilters: false,
variables: true,
variables: false,
sourceControl: false,
auditLogs: false,
showNonProdBanner: false,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,27 +1,15 @@
import { createApiKey, deleteApiKey, getApiKey } from '@/api/api-keys';
import {
getLdapConfig,
getLdapSynchronizations,
runLdapSync,
testLdapConnection,
updateLdapConfig,
} from '@/api/ldap';
import { getSettings, submitContactInfo } from '@/api/settings';
import * as publicApiApi from '@/api/api-keys';
import * as ldapApi from '@/api/ldap';
import * as settingsApi from '@/api/settings';
import { testHealthEndpoint } from '@/api/templates';
import type {
EnterpriseEditionFeatureValue,
ILdapConfig,
IN8nPromptResponse,
ISettingsState,
} from '@/Interface';
import type { ILdapConfig } from '@/Interface';
import { STORES, INSECURE_CONNECTION_WARNING } from '@/constants';
import { UserManagementAuthenticationMethod } from '@/Interface';
import type {
IDataObject,
LogLevel,
IN8nUISettings,
ITelemetrySettings,
WorkflowSettings,
IUserManagementSettings,
} from 'n8n-workflow';
import { ExpressionEvaluatorProxy } from 'n8n-workflow';
import { defineStore } from 'pinia';
@ -33,382 +21,422 @@ import { makeRestApiRequest } from '@/utils/apiUtils';
import { useTitleChange } from '@/composables/useTitleChange';
import { useToast } from '@/composables/useToast';
import { i18n } from '@/plugins/i18n';
import { computed, ref } from 'vue';
export const useSettingsStore = defineStore(STORES.SETTINGS, {
state: (): ISettingsState => ({
initialized: false,
settings: {} as IN8nUISettings,
userManagement: {
quota: -1,
showSetupOnFirstLoad: false,
smtpSetup: false,
authenticationMethod: UserManagementAuthenticationMethod.Email,
},
templatesEndpointHealthy: false,
api: {
export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const initialized = ref(false);
const settings = ref<IN8nUISettings>({} as IN8nUISettings);
const userManagement = ref<IUserManagementSettings>({
quota: -1,
showSetupOnFirstLoad: false,
smtpSetup: false,
authenticationMethod: UserManagementAuthenticationMethod.Email,
});
const templatesEndpointHealthy = ref(false);
const api = ref({
enabled: false,
latestVersion: 0,
path: '/',
swaggerUi: {
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,
},
},
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 {
return this.settings.versionCli;
},
nodeJsVersion(): string {
return this.settings.nodeJsVersion;
},
concurrency(): number {
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
const submitContactInfo = async (email: string) => {
try {
const usersStore = useUsersStore();
return await settingsApi.submitContactInfo(
settings.value.instanceId,
usersStore.currentUserId || '',
email,
);
},
telemetry(): ITelemetrySettings {
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;
}
} catch (error) {
return;
}
};
const { showToast } = useToast();
try {
await this.getSettings();
const testTemplatesEndpoint = async () => {
const timeout = new Promise((_, reject) => setTimeout(() => reject(), 2000));
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
useTitleChange().titleReset();
const createApiKey = async () => {
const rootStore = useRootStore();
const { apiKey } = await publicApiApi.createApiKey(rootStore.restApiContext);
return apiKey;
};
this.initialized = true;
} catch (e) {
showToast({
title: i18n.baseText('startupError'),
message: i18n.baseText('startupError.message'),
type: 'error',
duration: 0,
dangerouslyUseHTMLString: true,
});
const deleteApiKey = async () => {
const rootStore = useRootStore();
await publicApiApi.deleteApiKey(rootStore.restApiContext);
};
throw e;
}
},
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);
}
const getLdapConfig = async () => {
const rootStore = useRootStore();
return await ldapApi.getLdapConfig(rootStore.restApiContext);
};
if (
settings.authCookie.secure &&
location.protocol === 'http:' &&
!['localhost', '127.0.0.1'].includes(location.hostname)
) {
document.write(INSECURE_CONNECTION_WARNING);
return;
}
const getLdapSynchronizations = async (pagination: { page: number }) => {
const rootStore = useRootStore();
return await ldapApi.getLdapSynchronizations(rootStore.restApiContext, pagination);
};
const isV1BannerDismissedPermanently = (settings.banners?.dismissed || []).includes('V1');
if (!isV1BannerDismissedPermanently && useRootStore().versionCli.startsWith('1.')) {
useUIStore().pushBannerToStack('V1');
}
},
async getSettings(): Promise<void> {
const rootStore = useRootStore();
const settings = await getSettings(rootStore.restApiContext);
const testLdapConnection = async () => {
const rootStore = useRootStore();
return await ldapApi.testLdapConnection(rootStore.restApiContext);
};
this.setSettings(settings);
this.settings.communityNodesEnabled = settings.communityNodesEnabled;
this.setAllowedModules(settings.allowedModules);
this.setSaveDataErrorExecution(settings.saveDataErrorExecution);
this.setSaveDataSuccessExecution(settings.saveDataSuccessExecution);
this.setSaveDataProgressExecution(settings.saveExecutionProgress);
this.setSaveManualExecutions(settings.saveManualExecutions);
const updateLdapConfig = async (ldapConfig: ILdapConfig) => {
const rootStore = useRootStore();
return await ldapApi.updateLdapConfig(rootStore.restApiContext, ldapConfig);
};
rootStore.setUrlBaseWebhook(settings.urlBaseWebhook);
rootStore.setUrlBaseEditor(settings.urlBaseEditor);
rootStore.setEndpointForm(settings.endpointForm);
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);
const runLdapSync = async (data: IDataObject) => {
const rootStore = useRootStore();
return await ldapApi.runLdapSync(rootStore.restApiContext, data);
};
useVersionsStore().setVersionNotificationSettings(settings.versionNotifications);
},
stopShowingSetupPage(): void {
this.userManagement.showSetupOnFirstLoad = false;
},
disableTemplates(): void {
this.settings = {
...this.settings,
templates: {
...this.settings.templates,
enabled: false,
},
};
},
setAllowedModules(allowedModules: { builtIn?: string[]; external?: string[] }): void {
this.settings.allowedModules = allowedModules;
},
async submitContactInfo(email: string): Promise<IN8nPromptResponse | undefined> {
try {
const usersStore = useUsersStore();
return await submitContactInfo(
this.settings.instanceId,
usersStore.currentUserId || '',
email,
);
} catch (error) {
return;
}
},
async testTemplatesEndpoint(): Promise<void> {
const timeout = new Promise((_, reject) => setTimeout(() => reject(), 2000));
await Promise.race([testHealthEndpoint(this.templatesHost), timeout]);
this.templatesEndpointHealthy = true;
},
async getApiKey(): Promise<string | null> {
const rootStore = useRootStore();
const { apiKey } = await getApiKey(rootStore.restApiContext);
return apiKey;
},
async createApiKey(): Promise<string | null> {
const rootStore = useRootStore();
const { apiKey } = await createApiKey(rootStore.restApiContext);
return apiKey;
},
async deleteApiKey(): Promise<void> {
const rootStore = useRootStore();
await deleteApiKey(rootStore.restApiContext);
},
async getLdapConfig() {
const rootStore = useRootStore();
return await getLdapConfig(rootStore.restApiContext);
},
async getLdapSynchronizations(pagination: { page: number }) {
const rootStore = useRootStore();
return await getLdapSynchronizations(rootStore.restApiContext, pagination);
},
async testLdapConnection() {
const rootStore = useRootStore();
return await testLdapConnection(rootStore.restApiContext);
},
async updateLdapConfig(ldapConfig: ILdapConfig) {
const rootStore = useRootStore();
return await updateLdapConfig(rootStore.restApiContext, ldapConfig);
},
async runLdapSync(data: IDataObject) {
const rootStore = useRootStore();
return await runLdapSync(rootStore.restApiContext, data);
},
setSaveDataErrorExecution(newValue: WorkflowSettings.SaveDataExecution) {
this.saveDataErrorExecution = newValue;
},
setSaveDataSuccessExecution(newValue: WorkflowSettings.SaveDataExecution) {
this.saveDataSuccessExecution = newValue;
},
setSaveManualExecutions(saveManualExecutions: boolean) {
this.saveManualExecutions = saveManualExecutions;
},
setSaveDataProgressExecution(newValue: boolean) {
this.saveDataProgressExecution = newValue;
},
async getTimezones(): Promise<IDataObject> {
const rootStore = useRootStore();
return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/options/timezones');
},
},
const getTimezones = async (): Promise<IDataObject> => {
const rootStore = useRootStore();
return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/options/timezones');
};
const reset = () => {
settings.value = {} as IN8nUISettings;
};
return {
settings,
userManagement,
templatesEndpointHealthy,
api,
ldap,
saml,
mfa,
isDocker,
isDevRelease,
isEnterpriseFeatureEnabled,
databaseType,
planName,
consumerId,
binaryDataMode,
pruning,
security,
nodeJsVersion,
concurrency,
isPublicApiEnabled,
isSwaggerUIEnabled,
isPreviewMode,
publicApiLatestVersion,
publicApiPath,
isLdapLoginEnabled,
ldapLoginLabel,
isSamlLoginEnabled,
showSetupPage,
deploymentType,
isDesktopDeployment,
isCloudDeployment,
isSmtpSetup,
isPersonalizationSurveyEnabled,
telemetry,
logLevel,
isTelemetryEnabled,
isMfaFeatureEnabled,
areTagsEnabled,
isHiringBannerEnabled,
isTemplatesEnabled,
isTemplatesEndpointReachable,
templatesHost,
pushBackend,
isCommunityNodesFeatureEnabled,
isNpmAvailable,
allowedModules,
isQueueModeEnabled,
isWorkerViewAvailable,
isDefaultAuthenticationSaml,
workflowCallerPolicyDefaultOption,
permanentlyDismissedBanners,
isBelowUserQuota,
saveDataErrorExecution,
saveDataSuccessExecution,
saveManualExecutions,
saveDataProgressExecution,
isCommunityPlan,
reset,
testLdapConnection,
getLdapConfig,
getLdapSynchronizations,
updateLdapConfig,
runLdapSync,
getTimezones,
createApiKey,
getApiKey,
deleteApiKey,
testTemplatesEndpoint,
submitContactInfo,
disableTemplates,
stopShowingSetupPage,
getSettings,
setSettings,
initialize,
};
});

View file

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

View file

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

View file

@ -289,8 +289,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
},
async fetchTemplateById(templateId: string): Promise<ITemplatesWorkflowFull> {
const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = settingsStore.versionCli;
const versionCli: string = rootStore.versionCli;
const response = await getTemplateById(apiEndpoint, templateId, {
'n8n-version': versionCli,
});
@ -305,8 +306,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
},
async fetchCollectionById(collectionId: string): Promise<ITemplatesCollection | null> {
const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = settingsStore.versionCli;
const versionCli: string = rootStore.versionCli;
const response = await getCollectionById(apiEndpoint, collectionId, {
'n8n-version': versionCli,
});
@ -325,8 +327,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
return cachedCategories;
}
const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = settingsStore.versionCli;
const versionCli: string = rootStore.versionCli;
const response = await getCategories(apiEndpoint, { 'n8n-version': versionCli });
const categories = response.categories;
@ -340,8 +343,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
}
const settingsStore = useSettingsStore();
const rootStore = useRootStore();
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 collections = response.collections;
@ -361,8 +365,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
}
const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = settingsStore.versionCli;
const versionCli: string = rootStore.versionCli;
const payload = await getWorkflows(
apiEndpoint,
@ -402,8 +407,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
},
async getWorkflowTemplate(templateId: string): Promise<IWorkflowTemplate> {
const settingsStore = useSettingsStore();
const rootStore = useRootStore();
const apiEndpoint: string = settingsStore.templatesHost;
const versionCli: string = settingsStore.versionCli;
const versionCli: string = rootStore.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 settingsStore = useSettingsStore();
if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) {
if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]) {
await setWorkflowSharedWith(rootStore.restApiContext, payload.workflowId, {
shareWithIds: payload.sharedWithProjects.map((p) => p.id),
});

View file

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

View file

@ -12,8 +12,8 @@ export const isEnterpriseFeatureEnabled: RBACPermissionCheck<EnterprisePermissio
const settingsStore = useSettingsStore();
const mode = options.mode ?? 'allOf';
if (mode === 'allOf') {
return features.every(settingsStore.isEnterpriseFeatureEnabled);
return features.every((feature) => settingsStore.isEnterpriseFeatureEnabled[feature]);
} 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 { type RouteLocationNormalized } from 'vue-router';
import type { EnterprisePermissionOptions } from '@/types/rbac';
vi.mock('@/stores/settings.store', () => ({
useSettingsStore: vi.fn(),
}));
import { createPinia, setActivePinia } from 'pinia';
import { defaultSettings } from '@/__tests__/defaults';
describe('Middleware', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
describe('enterprise', () => {
it('should redirect to homepage if none of the required features are enabled in allOf mode', async () => {
vi.mocked(useSettingsStore).mockReturnValue({
isEnterpriseFeatureEnabled: (_) => false,
} as ReturnType<typeof useSettingsStore>);
useSettingsStore().settings.enterprise = {
...defaultSettings.enterprise,
[EnterpriseEditionFeature.Ldap]: false,
[EnterpriseEditionFeature.Saml]: false,
};
const nextMock = vi.fn();
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 () => {
vi.mocked(useSettingsStore).mockReturnValue({
isEnterpriseFeatureEnabled: (feature) =>
[EnterpriseEditionFeature.Saml, EnterpriseEditionFeature.Ldap].includes(feature),
} as ReturnType<typeof useSettingsStore>);
useSettingsStore().settings.enterprise = {
...defaultSettings.enterprise,
[EnterpriseEditionFeature.Ldap]: true,
[EnterpriseEditionFeature.Saml]: true,
};
const nextMock = vi.fn();
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 () => {
vi.mocked(useSettingsStore).mockReturnValue({
isEnterpriseFeatureEnabled: (_) => false,
} as ReturnType<typeof useSettingsStore>);
useSettingsStore().settings.enterprise = {
...defaultSettings.enterprise,
[EnterpriseEditionFeature.Saml]: false,
};
const nextMock = vi.fn();
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 () => {
vi.mocked(useSettingsStore).mockReturnValue({
isEnterpriseFeatureEnabled: (feature) => feature === EnterpriseEditionFeature.Saml,
} as ReturnType<typeof useSettingsStore>);
useSettingsStore().settings.enterprise = {
...defaultSettings.enterprise,
[EnterpriseEditionFeature.Ldap]: true,
[EnterpriseEditionFeature.Saml]: false,
};
const nextMock = vi.fn();
const options: EnterprisePermissionOptions = {

View file

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

View file

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

View file

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

View file

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

View file

@ -76,7 +76,7 @@ const usersListActions = computed((): IUserListAction[] => {
];
});
const isAdvancedPermissionsEnabled = computed((): boolean => {
return settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.AdvancedPermissions);
return settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.AdvancedPermissions];
});
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 { useRBACStore } from '@/stores/rbac.store';
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', () => {
let server: ReturnType<typeof setupServer>;
@ -16,7 +18,13 @@ describe('VariablesView', () => {
let usersStore: ReturnType<typeof useUsersStore>;
let rbacStore: ReturnType<typeof useRBACStore>;
const renderComponent = createComponentRenderer(VariablesView);
const renderComponent = createComponentRenderer(VariablesView, {
pinia: createTestingPinia({
initialState: {
[STORES.SETTINGS]: SETTINGS_STORE_DEFAULT_STATE,
},
}),
});
beforeAll(() => {
server = setupServer();
@ -105,6 +113,7 @@ describe('VariablesView', () => {
});
it('should render variable entries', async () => {
settingsStore.settings.enterprise[EnterpriseEditionFeature.Variables] = true;
server.createList('variable', 3);
const wrapper = renderComponent({ pinia });

View file

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

View file

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

View file

@ -11,8 +11,10 @@ import { useUsersStore } from '@/stores/users.store';
import { createUser } from '@/__tests__/data/users';
import { createProjectListItem } from '@/__tests__/data/projects';
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 { useSettingsStore } from '@/stores/settings.store';
import { defaultSettings } from '@/__tests__/defaults';
const wrapperComponentWithModal = {
components: { SettingsUsersView, ModalRoot, DeleteUserModal },
@ -47,6 +49,11 @@ describe('SettingsUsersView', () => {
usersStore = useUsersStore();
rbacStore = useRBACStore();
useSettingsStore().settings.enterprise = {
...defaultSettings.enterprise,
[EnterpriseEditionFeature.AdvancedExecutionFilters]: true,
};
vi.spyOn(rbacStore, 'hasScope').mockReturnValue(true);
vi.spyOn(usersApi, 'getUsers').mockResolvedValue(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 interface IN8nUISettings {
isDocker: boolean;
isDocker?: boolean;
databaseType: 'sqlite' | 'mariadb' | 'mysqldb' | 'postgresdb';
endpointForm: string;
endpointFormTest: string;