diff --git a/README.md b/README.md index c2519833e..077d46d12 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,21 @@ Not needed if you're using Keycloak. #### `COMPONENT_REPOSITORY_UI_ENABLED` (boolean, default: `false`) a boolean used to determine whether the Component Repository (former Digital Exchange) should be enabled or not. +#### `ENTANDO_FEATURE_FLAGS` (string, default: `""`) +a comma-separated list of feature flags to enable. Available flags: + +- `HEADLESS_WIDGET_CONFIG` — enables legacy widget configuration via headless Struts iframe. When enabled, widgets with `configUi.customElement` set to `LEGACY_CONFIG` will render their configuration form inside an iframe pointing to the Struts admin console. + +Example with a single flag: +``` +ENTANDO_FEATURE_FLAGS=HEADLESS_WIDGET_CONFIG +``` + +Example with multiple flags: +``` +ENTANDO_FEATURE_FLAGS=HEADLESS_WIDGET_CONFIG,ANOTHER_FLAG +``` + #### `KEYCLOAK_ENABLED` (boolean, default: `false`) a boolean that enables authentication through [Keycloak](https://www.keycloak.org/). Won't be used if `USE_MOCKS` is set to true. diff --git a/config/env.js b/config/env.js index 31eae0045..8b294dd52 100644 --- a/config/env.js +++ b/config/env.js @@ -61,6 +61,7 @@ const REACT_APP = /^REACT_APP_/i; const RUNTIME_OVERRIDABLE_VARS = [ 'COMPONENT_REPOSITORY_UI_ENABLED', 'DOMAIN', + 'ENTANDO_FEATURE_FLAGS', 'KEYCLOAK_JSON', ]; diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 886238d39..a9d5b3391 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -14,7 +14,8 @@ then else ENV_KEYCLOAK_JSON="${KEYCLOAK_JSON}" fi -ENV_JSON='{"DOMAIN":"'"$ENV_DOMAIN"'", "KEYCLOAK_JSON":"'"$ENV_KEYCLOAK_JSON"'"}' +ENV_FEATURE_FLAGS=${ENTANDO_FEATURE_FLAGS:-""} +ENV_JSON='{"DOMAIN":"'"$ENV_DOMAIN"'", "KEYCLOAK_JSON":"'"$ENV_KEYCLOAK_JSON"'", "ENTANDO_FEATURE_FLAGS":"'"$ENV_FEATURE_FLAGS"'"}' ESCAPED_ENV_JSON=$(echo $ENV_JSON | sed 's/\"/\\\"/g' | sed 's/\//\\\//g' | tr -d '\n' | tr -d '[[:blank:]]') sed -i 's/"REACT_APP_ENV"/'"$ESCAPED_ENV_JSON"'/g' $HOME/app-builder/index.html diff --git a/src/helpers/featureFlags.js b/src/helpers/featureFlags.js new file mode 100644 index 000000000..143c40f01 --- /dev/null +++ b/src/helpers/featureFlags.js @@ -0,0 +1,23 @@ +const getProcessEnvVar = envVar => process.env[envVar] || ''; + +const getWindowEnvVar = envVar => (window && window.env && window.env[envVar] ? window.env[envVar] : ''); + +const getRawValue = (envVar) => { + if (process.env.NODE_ENV === 'development') { + return getProcessEnvVar(envVar); + } + return getWindowEnvVar(envVar) || getProcessEnvVar(envVar); +}; + +const FEATURE_FLAGS_KEY = 'ENTANDO_FEATURE_FLAGS'; + +export const getFeatureFlags = () => { + const raw = getRawValue(FEATURE_FLAGS_KEY); + if (!raw) return []; + return raw.split(',').map(f => f.trim()).filter(Boolean); +}; + +export const hasFeatureFlag = flag => getFeatureFlags().includes(flag); + +// Known feature flags +export const FEATURE_FLAG_HEADLESS_WIDGET_CONFIG = 'HEADLESS_WIDGET_CONFIG'; diff --git a/src/helpers/legacyWidget.js b/src/helpers/legacyWidget.js index c0007c163..0e0e6e0ed 100644 --- a/src/helpers/legacyWidget.js +++ b/src/helpers/legacyWidget.js @@ -1,11 +1,16 @@ import getAppBuilderWidgetForm from 'helpers/getAppBuilderWidgetForm'; import { adminConsoleUrl } from 'helpers/urlUtils'; +import { hasFeatureFlag, FEATURE_FLAG_HEADLESS_WIDGET_CONFIG } from 'helpers/featureFlags'; export const LEGACY_CONFIG_CUSTOM_ELEMENT = 'LEGACY_CONFIG'; // A widget is "legacy" when its configUi.customElement is set to LEGACY_CONFIG // and it has no internal app-builder form registered in getAppBuilderWidgetForm. +// Gated behind the HEADLESS_WIDGET_CONFIG feature flag. export const isLegacyWidget = (widget) => { + if (!hasFeatureFlag(FEATURE_FLAG_HEADLESS_WIDGET_CONFIG)) { + return false; + } if (!widget || !widget.configUi || widget.configUi.customElement !== 'LEGACY_CONFIG') { return false; } diff --git a/test/helpers/featureFlags.test.js b/test/helpers/featureFlags.test.js new file mode 100644 index 000000000..175f2aea3 --- /dev/null +++ b/test/helpers/featureFlags.test.js @@ -0,0 +1,75 @@ +import { getFeatureFlags, hasFeatureFlag, FEATURE_FLAG_HEADLESS_WIDGET_CONFIG } from 'helpers/featureFlags'; + +describe('helpers/featureFlags', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv, NODE_ENV: 'development' }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + describe('getFeatureFlags', () => { + it('returns empty array when env var is not set', () => { + delete process.env.ENTANDO_FEATURE_FLAGS; + expect(getFeatureFlags()).toEqual([]); + }); + + it('returns empty array when env var is empty', () => { + process.env.ENTANDO_FEATURE_FLAGS = ''; + expect(getFeatureFlags()).toEqual([]); + }); + + it('parses a single flag', () => { + process.env.ENTANDO_FEATURE_FLAGS = 'HEADLESS_WIDGET_CONFIG'; + expect(getFeatureFlags()).toEqual(['HEADLESS_WIDGET_CONFIG']); + }); + + it('parses multiple comma-separated flags', () => { + process.env.ENTANDO_FEATURE_FLAGS = 'HEADLESS_WIDGET_CONFIG,OTHER_FLAG,THIRD'; + expect(getFeatureFlags()).toEqual(['HEADLESS_WIDGET_CONFIG', 'OTHER_FLAG', 'THIRD']); + }); + + it('trims whitespace around flags', () => { + process.env.ENTANDO_FEATURE_FLAGS = ' HEADLESS_WIDGET_CONFIG , OTHER_FLAG '; + expect(getFeatureFlags()).toEqual(['HEADLESS_WIDGET_CONFIG', 'OTHER_FLAG']); + }); + + it('ignores empty segments from trailing commas', () => { + process.env.ENTANDO_FEATURE_FLAGS = 'FLAG_A,,FLAG_B,'; + expect(getFeatureFlags()).toEqual(['FLAG_A', 'FLAG_B']); + }); + }); + + describe('hasFeatureFlag', () => { + it('returns true when the flag is present', () => { + process.env.ENTANDO_FEATURE_FLAGS = 'HEADLESS_WIDGET_CONFIG'; + expect(hasFeatureFlag(FEATURE_FLAG_HEADLESS_WIDGET_CONFIG)).toBe(true); + }); + + it('returns false when the flag is not present', () => { + process.env.ENTANDO_FEATURE_FLAGS = 'OTHER_FLAG'; + expect(hasFeatureFlag(FEATURE_FLAG_HEADLESS_WIDGET_CONFIG)).toBe(false); + }); + + it('returns false when no flags are set', () => { + delete process.env.ENTANDO_FEATURE_FLAGS; + expect(hasFeatureFlag(FEATURE_FLAG_HEADLESS_WIDGET_CONFIG)).toBe(false); + }); + + it('works with multiple flags', () => { + process.env.ENTANDO_FEATURE_FLAGS = 'FOO,HEADLESS_WIDGET_CONFIG,BAR'; + expect(hasFeatureFlag(FEATURE_FLAG_HEADLESS_WIDGET_CONFIG)).toBe(true); + expect(hasFeatureFlag('FOO')).toBe(true); + expect(hasFeatureFlag('UNKNOWN')).toBe(false); + }); + }); + + describe('FEATURE_FLAG_HEADLESS_WIDGET_CONFIG', () => { + it('has the expected value', () => { + expect(FEATURE_FLAG_HEADLESS_WIDGET_CONFIG).toBe('HEADLESS_WIDGET_CONFIG'); + }); + }); +}); \ No newline at end of file