mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-01 15:47:41 +08:00
fix(editor): Gate dynamic credentials UI behind license check (#25464)
This commit is contained in:
@@ -118,7 +118,9 @@ describe('WorkflowCard', () => {
|
||||
envFeatureFlags: {
|
||||
N8N_ENV_FEAT_DYNAMIC_CREDENTIALS: true,
|
||||
},
|
||||
activeModules: ['dynamic-credentials'],
|
||||
} as unknown as FrontendSettings;
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(true);
|
||||
|
||||
windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
|
||||
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
|
||||
import { useWorkflowActivate } from '@/app/composables/useWorkflowActivate';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
|
||||
const WORKFLOW_LIST_ITEM_ACTIONS = {
|
||||
OPEN: 'open',
|
||||
@@ -109,7 +109,7 @@ const router = useRouter();
|
||||
const route = useRoute();
|
||||
const telemetry = useTelemetry();
|
||||
const mcp = useMcp();
|
||||
const { check: checkEnvFeatureFlag } = useEnvFeatureFlag();
|
||||
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
|
||||
const uiStore = useUIStore();
|
||||
const usersStore = useUsersStore();
|
||||
@@ -293,10 +293,6 @@ const isWorkflowPublished = computed(() => {
|
||||
return props.data.activeVersionId !== null;
|
||||
});
|
||||
|
||||
const isDynamicCredentialsEnabled = computed(() =>
|
||||
checkEnvFeatureFlag.value('DYNAMIC_CREDENTIALS'),
|
||||
);
|
||||
|
||||
const hasDynamicCredentials = computed(() => {
|
||||
return isDynamicCredentialsEnabled.value && props.data.hasResolvableCredentials;
|
||||
});
|
||||
|
||||
@@ -80,8 +80,10 @@ describe('WorkflowSettingsVue', () => {
|
||||
envFeatureFlags: {
|
||||
N8N_ENV_FEAT_DYNAMIC_CREDENTIALS: true,
|
||||
},
|
||||
activeModules: ['dynamic-credentials'],
|
||||
releaseChannel: 'stable',
|
||||
});
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(true);
|
||||
workflowsStore.workflowName = 'Test Workflow';
|
||||
workflowsStore.workflowId = '1';
|
||||
// Populate workflowsById to mark workflow as existing (not new)
|
||||
|
||||
@@ -45,8 +45,8 @@ import { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
|
||||
import { useGlobalLinkActions } from '@/app/composables/useGlobalLinkActions';
|
||||
import { useNodeCreatorStore } from '@/features/shared/nodeCreator/nodeCreator.store';
|
||||
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
|
||||
import { useCredentialResolvers } from '@/features/resolvers/composables/useCredentialResolvers';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
|
||||
import { ElCol, ElRow, ElSwitch } from 'element-plus';
|
||||
|
||||
@@ -58,7 +58,7 @@ const modalBus = createEventBus();
|
||||
const telemetry = useTelemetry();
|
||||
const { isEligibleForMcpAccess, trackMcpAccessEnabledForWorkflow, mcpTriggerMap } = useMcp();
|
||||
const { registerCustomAction, unregisterCustomAction } = useGlobalLinkActions();
|
||||
const { check: checkEnvFeatureFlag } = useEnvFeatureFlag();
|
||||
const { isEnabled: isCredentialResolverEnabled } = useDynamicCredentials();
|
||||
|
||||
const rootStore = useRootStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
@@ -144,9 +144,6 @@ const executionLogic = computed(() => {
|
||||
const isMCPEnabled = computed(
|
||||
() => settingsStore.isModuleActive('mcp') && settingsStore.moduleSettings.mcp?.mcpAccessEnabled,
|
||||
);
|
||||
const isCredentialResolverEnabled = computed(() =>
|
||||
checkEnvFeatureFlag.value('DYNAMIC_CREDENTIALS'),
|
||||
);
|
||||
const readOnlyEnv = computed(
|
||||
() => sourceControlStore.preferences.branchReadOnly || collaborationStore.shouldBeReadOnly,
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useRecentResources } from '@/features/shared/commandBar/composables/use
|
||||
import { usePostHog } from '@/app/stores/posthog.store';
|
||||
import { TEMPLATE_SETUP_EXPERIENCE } from '@/app/constants/experiments';
|
||||
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
|
||||
const ChangePasswordView = async () =>
|
||||
await import('@/features/core/auth/views/ChangePasswordView.vue');
|
||||
@@ -679,8 +680,8 @@ export const routes: RouteRecordRaw[] = [
|
||||
middleware: ['authenticated', 'custom'],
|
||||
middlewareOptions: {
|
||||
custom: () => {
|
||||
const { check } = useEnvFeatureFlag();
|
||||
return check.value('DYNAMIC_CREDENTIALS');
|
||||
const { isEnabled } = useDynamicCredentials();
|
||||
return isEnabled.value;
|
||||
},
|
||||
},
|
||||
telemetry: {
|
||||
|
||||
@@ -37,7 +37,9 @@ describe('CredentialCard', () => {
|
||||
envFeatureFlags: {
|
||||
N8N_ENV_FEAT_DYNAMIC_CREDENTIALS: true,
|
||||
},
|
||||
activeModules: ['dynamic-credentials'],
|
||||
} as unknown as FrontendSettings;
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('should render name and home project name', () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import ProjectCardBadge from '@/features/collaboration/projects/components/Proje
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { ResourceType } from '@/features/collaboration/projects/projects.utils';
|
||||
import type { CredentialsResource } from '@/Interface';
|
||||
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
|
||||
import {
|
||||
N8nActionToggle,
|
||||
@@ -51,11 +51,7 @@ const message = useMessage();
|
||||
const uiStore = useUIStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const { check: checkEnvFeatureFlag } = useEnvFeatureFlag();
|
||||
|
||||
const isDynamicCredentialsEnabled = computed(() =>
|
||||
checkEnvFeatureFlag.value('DYNAMIC_CREDENTIALS'),
|
||||
);
|
||||
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
|
||||
const resourceTypeLabel = computed(() => locale.baseText('generic.credential').toLowerCase());
|
||||
const credentialType = computed(() =>
|
||||
|
||||
+2
-6
@@ -65,7 +65,7 @@ import {
|
||||
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { setParameterValue } from '@/app/utils/parameterUtils';
|
||||
import get from 'lodash/get';
|
||||
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
|
||||
type Props = {
|
||||
modalName: string;
|
||||
@@ -92,7 +92,7 @@ const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
const router = useRouter();
|
||||
const rootStore = useRootStore();
|
||||
const { check: checkEnvFeatureFlag } = useEnvFeatureFlag();
|
||||
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
|
||||
const activeTab = ref('connection');
|
||||
const authError = ref('');
|
||||
@@ -352,10 +352,6 @@ const homeProject = computed(() => {
|
||||
return currentProject ?? personalProject;
|
||||
});
|
||||
|
||||
const isDynamicCredentialsEnabled = computed<boolean>(() => {
|
||||
return checkEnvFeatureFlag.value('DYNAMIC_CREDENTIALS');
|
||||
});
|
||||
|
||||
const isNewCredential = computed(() => props.mode === 'new' && !credentialId.value);
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
+2
@@ -97,7 +97,9 @@ describe('NodeCredentials', () => {
|
||||
envFeatureFlags: {
|
||||
N8N_ENV_FEAT_DYNAMIC_CREDENTIALS: true,
|
||||
},
|
||||
activeModules: ['dynamic-credentials'],
|
||||
} as unknown as FrontendSettings;
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(true);
|
||||
|
||||
beforeAll(() => {
|
||||
credentialsStore.state.credentialTypes = {
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { isEmpty } from '@/app/utils/typesUtils';
|
||||
import { getResourcePermissions } from '@n8n/permissions';
|
||||
import { useNodeCredentialOptions } from '../composables/useNodeCredentialOptions';
|
||||
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
|
||||
import {
|
||||
N8nBadge,
|
||||
@@ -80,7 +80,7 @@ const uiStore = useUIStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const workflowState = injectWorkflowState();
|
||||
const { check: checkEnvFeatureFlag } = useEnvFeatureFlag();
|
||||
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
|
||||
const canCreateCredentials = computed(
|
||||
() =>
|
||||
@@ -130,10 +130,6 @@ const selected = computed<Record<string, INodeCredentialsDetails>>(
|
||||
() => props.node.credentials ?? {},
|
||||
);
|
||||
|
||||
const isDynamicCredentialsEnabled = computed(() =>
|
||||
checkEnvFeatureFlag.value('DYNAMIC_CREDENTIALS'),
|
||||
);
|
||||
|
||||
const hasWorkflowResolver = computed(() => {
|
||||
return !!workflowsStore.workflowSettings?.credentialResolverId;
|
||||
});
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { useDynamicCredentials } from './useDynamicCredentials';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import type { FrontendSettings } from '@n8n/api-types';
|
||||
|
||||
describe('useDynamicCredentials', () => {
|
||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
|
||||
const setup = ({
|
||||
moduleActive,
|
||||
featureFlag,
|
||||
}: {
|
||||
moduleActive: boolean;
|
||||
featureFlag: boolean;
|
||||
}) => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
settingsStore = useSettingsStore();
|
||||
settingsStore.settings = {
|
||||
envFeatureFlags: {
|
||||
N8N_ENV_FEAT_DYNAMIC_CREDENTIALS: featureFlag,
|
||||
},
|
||||
activeModules: moduleActive ? ['dynamic-credentials'] : [],
|
||||
} as unknown as FrontendSettings;
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockImplementation(
|
||||
(name: string) => moduleActive && name === 'dynamic-credentials',
|
||||
);
|
||||
|
||||
return useDynamicCredentials();
|
||||
};
|
||||
|
||||
it('should be enabled when both module is active and feature flag is on', () => {
|
||||
const { isEnabled } = setup({ moduleActive: true, featureFlag: true });
|
||||
expect(isEnabled.value).toBe(true);
|
||||
});
|
||||
|
||||
it('should be disabled when module is not active', () => {
|
||||
const { isEnabled } = setup({ moduleActive: false, featureFlag: true });
|
||||
expect(isEnabled.value).toBe(false);
|
||||
});
|
||||
|
||||
it('should be disabled when feature flag is off', () => {
|
||||
const { isEnabled } = setup({ moduleActive: true, featureFlag: false });
|
||||
expect(isEnabled.value).toBe(false);
|
||||
});
|
||||
|
||||
it('should be disabled when both module is not active and feature flag is off', () => {
|
||||
const { isEnabled } = setup({ moduleActive: false, featureFlag: false });
|
||||
expect(isEnabled.value).toBe(false);
|
||||
});
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { computed } from 'vue';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
|
||||
|
||||
export const useDynamicCredentials = () => {
|
||||
const settingsStore = useSettingsStore();
|
||||
const { check } = useEnvFeatureFlag();
|
||||
|
||||
const isEnabled = computed(
|
||||
() => settingsStore.isModuleActive('dynamic-credentials') && check.value('DYNAMIC_CREDENTIALS'),
|
||||
);
|
||||
|
||||
return { isEnabled };
|
||||
};
|
||||
+8
-7
@@ -9,23 +9,24 @@ import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import CanvasNodeSettingsIcons from './CanvasNodeSettingsIcons.vue';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
import { computed } from 'vue';
|
||||
|
||||
vi.mock('@/features/shared/envFeatureFlag/useEnvFeatureFlag', () => ({
|
||||
useEnvFeatureFlag: vi.fn(),
|
||||
vi.mock('@/features/resolvers/composables/useDynamicCredentials', () => ({
|
||||
useDynamicCredentials: vi.fn(),
|
||||
}));
|
||||
|
||||
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
|
||||
const mockedUseEnvFeatureFlag = vi.mocked(useEnvFeatureFlag);
|
||||
const mockedUseDynamicCredentials = vi.mocked(useDynamicCredentials);
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasNodeSettingsIcons, {
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
|
||||
const mockFeatureFlag = (enabled: boolean) => {
|
||||
mockedUseEnvFeatureFlag.mockReturnValue({
|
||||
check: { value: (flag: string) => enabled && flag === 'DYNAMIC_CREDENTIALS' },
|
||||
} as unknown as ReturnType<typeof useEnvFeatureFlag>);
|
||||
mockedUseDynamicCredentials.mockReturnValue({
|
||||
isEnabled: computed(() => enabled),
|
||||
} as ReturnType<typeof useDynamicCredentials>);
|
||||
};
|
||||
|
||||
describe('CanvasNodeSettingsIcons', () => {
|
||||
|
||||
+2
-6
@@ -4,18 +4,14 @@ import { useCanvasNode } from '../../../../../composables/useCanvasNode';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
|
||||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
|
||||
import { N8nIcon, N8nTooltip } from '@n8n/design-system';
|
||||
const { name } = useCanvasNode();
|
||||
const i18n = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const { check: checkEnvFeatureFlag } = useEnvFeatureFlag();
|
||||
|
||||
const isDynamicCredentialsEnabled = computed(() =>
|
||||
checkEnvFeatureFlag.value('DYNAMIC_CREDENTIALS'),
|
||||
);
|
||||
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
|
||||
|
||||
const node = computed(() => workflowsStore.workflowObject.getNode(name.value));
|
||||
const size = 'medium';
|
||||
|
||||
Reference in New Issue
Block a user