mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Using special env vars as feature flags in the frontend (#17355)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -153,4 +153,5 @@ export const defaultSettings: FrontendSettings = {
|
||||
quota: 0,
|
||||
},
|
||||
activeModules: [],
|
||||
envFeatureFlags: {},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import type { FrontendSettings, N8nEnvFeatFlags, N8nEnvFeatFlagValue } from '@n8n/api-types';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { mockedStore, type MockedStore } from '@/__tests__/utils';
|
||||
import EnvFeatureFlag from '@/features/env-feature-flag/EnvFeatureFlag.vue';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
|
||||
const renderComponent = createComponentRenderer(EnvFeatureFlag);
|
||||
|
||||
describe('EnvFeatureFlag', () => {
|
||||
let settingsStore: MockedStore<typeof useSettingsStore>;
|
||||
const originalEnv = { ...import.meta.env };
|
||||
|
||||
beforeEach(() => {
|
||||
Object.keys(import.meta.env).forEach((key) => {
|
||||
if (key.startsWith('N8N_ENV_FEAT_')) {
|
||||
delete (import.meta.env as N8nEnvFeatFlags)[key as keyof N8nEnvFeatFlags];
|
||||
}
|
||||
});
|
||||
createTestingPinia();
|
||||
|
||||
settingsStore = mockedStore(useSettingsStore);
|
||||
settingsStore.settings = {
|
||||
envFeatureFlags: {},
|
||||
} as FrontendSettings;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.assign(import.meta.env, originalEnv);
|
||||
});
|
||||
|
||||
test.each<[N8nEnvFeatFlagValue, Uppercase<string>, boolean]>([
|
||||
// Truthy values that should render content
|
||||
['true', 'TEST_FLAG', true],
|
||||
['enabled', 'TEST_FLAG', true],
|
||||
['yes', 'TEST_FLAG', true],
|
||||
['1', 'TEST_FLAG', true],
|
||||
['on', 'TEST_FLAG', true],
|
||||
[true, 'TEST_FLAG', true],
|
||||
|
||||
// Falsy values that should not render content
|
||||
['false', 'TEST_FLAG', false],
|
||||
[false, 'TEST_FLAG', false],
|
||||
['', 'TEST_FLAG', false],
|
||||
[undefined, 'TEST_FLAG', false],
|
||||
[0, 'TEST_FLAG', false],
|
||||
])(
|
||||
'should %s render slot content when feature flag value is %s',
|
||||
(value, flagName, shouldRender) => {
|
||||
const envKey: keyof N8nEnvFeatFlags = `N8N_ENV_FEAT_${flagName}`;
|
||||
|
||||
settingsStore.settings.envFeatureFlags = {
|
||||
[envKey]: value,
|
||||
};
|
||||
|
||||
const { container } = renderComponent({
|
||||
props: {
|
||||
name: flagName,
|
||||
},
|
||||
slots: {
|
||||
default: '<div data-testid="slot-content">Feature content</div>',
|
||||
},
|
||||
});
|
||||
|
||||
if (shouldRender) {
|
||||
expect(container.querySelector('[data-testid="slot-content"]')).toBeTruthy();
|
||||
} else {
|
||||
expect(container.querySelector('[data-testid="slot-content"]')).toBeNull();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it('should work with different flag names', () => {
|
||||
settingsStore.settings.envFeatureFlags = {
|
||||
N8N_ENV_FEAT_WORKFLOW_DIFFS: 'true',
|
||||
N8N_ENV_FEAT_ANOTHER_FEATURE: 'false',
|
||||
};
|
||||
|
||||
const { container: container1 } = renderComponent({
|
||||
props: {
|
||||
name: 'WORKFLOW_DIFFS',
|
||||
},
|
||||
slots: {
|
||||
default: '<div data-testid="feature-1">Feature 1</div>',
|
||||
},
|
||||
});
|
||||
|
||||
const { container: container2 } = renderComponent({
|
||||
props: {
|
||||
name: 'ANOTHER_FEATURE',
|
||||
},
|
||||
slots: {
|
||||
default: '<div data-testid="feature-2">Feature 2</div>',
|
||||
},
|
||||
});
|
||||
|
||||
expect(container1.querySelector('[data-testid="feature-1"]')).toBeTruthy();
|
||||
expect(container2.querySelector('[data-testid="feature-2"]')).toBeNull();
|
||||
});
|
||||
|
||||
describe('runtime vs build-time priority', () => {
|
||||
it('should prioritize runtime settings over build-time env vars', () => {
|
||||
// Set build-time env var
|
||||
(import.meta.env as N8nEnvFeatFlags).N8N_ENV_FEAT_TEST_FLAG = 'true';
|
||||
|
||||
// Set runtime setting to override
|
||||
settingsStore.settings.envFeatureFlags = {
|
||||
N8N_ENV_FEAT_TEST_FLAG: 'false',
|
||||
};
|
||||
|
||||
const { container } = renderComponent({
|
||||
props: {
|
||||
name: 'TEST_FLAG',
|
||||
},
|
||||
slots: {
|
||||
default: '<div data-testid="slot-content">Feature content</div>',
|
||||
},
|
||||
});
|
||||
|
||||
// Should use runtime value (false) over build-time value (true)
|
||||
expect(container.querySelector('[data-testid="slot-content"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('should fallback to build-time env vars when runtime settings are not available', () => {
|
||||
// Set build-time env var
|
||||
(import.meta.env as N8nEnvFeatFlags).N8N_ENV_FEAT_TEST_FLAG = 'true';
|
||||
|
||||
// Runtime settings are empty
|
||||
settingsStore.settings.envFeatureFlags = {};
|
||||
|
||||
const { container } = renderComponent({
|
||||
props: {
|
||||
name: 'TEST_FLAG',
|
||||
},
|
||||
slots: {
|
||||
default: '<div data-testid="slot-content">Feature content</div>',
|
||||
},
|
||||
});
|
||||
|
||||
// Should use build-time value
|
||||
expect(container.querySelector('[data-testid="slot-content"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false when neither runtime nor build-time values are set', () => {
|
||||
// No runtime setting
|
||||
settingsStore.settings.envFeatureFlags = {};
|
||||
|
||||
// No build-time env var (already cleared in beforeEach)
|
||||
|
||||
const { container } = renderComponent({
|
||||
props: {
|
||||
name: 'TEST_FLAG',
|
||||
},
|
||||
slots: {
|
||||
default: '<div data-testid="slot-content">Feature content</div>',
|
||||
},
|
||||
});
|
||||
|
||||
// Should default to false
|
||||
expect(container.querySelector('[data-testid="slot-content"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { useEnvFeatureFlag } from '@/features/env-feature-flag/useEnvFeatureFlag';
|
||||
|
||||
/*
|
||||
EnvFeatureFlag conditionally renders content based on environment variable based feature flags
|
||||
Environment variable feature flags are defined in form of `N8N_ENV_FEAT_<FEATURE_NAME>`
|
||||
The component's name property should be in uppercase and match the environment variable name without the prefix
|
||||
Usage example: <EnvFeatureFlag name="FEATURE_NAME"> Feature content </EnvFeatureFlag>
|
||||
*/
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EnvFeatureFlag',
|
||||
props: {
|
||||
name: {
|
||||
type: String as () => Uppercase<string>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const envFeatureFlag = useEnvFeatureFlag();
|
||||
const isEnabled = computed(() => envFeatureFlag.check.value(props.name));
|
||||
|
||||
return () => (isEnabled.value && slots.default ? slots.default() : null);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { computed } from 'vue';
|
||||
import type { N8nEnvFeatFlags } from '@n8n/api-types';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
|
||||
export const useEnvFeatureFlag = () => {
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const check = computed(() => (flag: Uppercase<string>): boolean => {
|
||||
const key = `N8N_ENV_FEAT_${flag}` as const;
|
||||
|
||||
// Settings provided by the backend take precedence over build-time or runtime flags
|
||||
const settingsProvidedEnvFeatFlag = settingsStore.settings.envFeatureFlags?.[key];
|
||||
if (settingsProvidedEnvFeatFlag !== undefined) {
|
||||
return settingsProvidedEnvFeatFlag !== 'false' && !!settingsProvidedEnvFeatFlag;
|
||||
}
|
||||
|
||||
// "Vite exposes certain constants under the special import.meta.env object. These constants are defined as global variables during dev and statically replaced at build time to make tree-shaking effective."
|
||||
// See https://vite.dev/guide/env-and-mode.html
|
||||
const buildTimeValue = (import.meta.env as N8nEnvFeatFlags)[key];
|
||||
if (buildTimeValue !== undefined) {
|
||||
return buildTimeValue !== 'false' && !!buildTimeValue;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
check,
|
||||
};
|
||||
};
|
||||
@@ -141,7 +141,7 @@ export default mergeConfig(
|
||||
plugins,
|
||||
resolve: { alias },
|
||||
base: publicPath,
|
||||
envPrefix: 'VUE',
|
||||
envPrefix: ['VUE', 'N8N_ENV_FEAT'],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
|
||||
Reference in New Issue
Block a user