|
| 1 | +/** |
| 2 | + * Feature flag configuration array. Each entry defines how a feature's availability |
| 3 | + * is computed from platform settings and team type properties. |
| 4 | + * |
| 5 | + * For each entry, `buildFeatureChecks` produces up to three computed properties: |
| 6 | + * - `{output}ForPlatform` - platform-level check result (only if `platformKey` is set) |
| 7 | + * - `{output}ForTeam` - team-level check result (only if `teamKey` is set) |
| 8 | + * - `{output}` - combined result (see combination rules below) |
| 9 | + * |
| 10 | + * @property {string} output - Name of the computed property for the combined result. |
| 11 | + * Always required. Convention: `is{Feature}FeatureEnabled`. |
| 12 | + * |
| 13 | + * @property {string} [platformKey] - Key to look up in the platform features object |
| 14 | + * (state.features). If truthy, the platform check passes. |
| 15 | + * |
| 16 | + * @property {string} [teamKey] - Key to look up in team type properties |
| 17 | + * (team.type.properties.features). If truthy or if the team type has |
| 18 | + * `enableAllFeatures: true`, the team check passes. |
| 19 | + * |
| 20 | + * @property {boolean} [optOut=false] - Controls the default behavior of the team check. |
| 21 | + * - `false` (default, opt-in): feature is disabled unless the team type explicitly |
| 22 | + * enables it or has `enableAllFeatures: true`. |
| 23 | + * - `true` (opt-out): feature is enabled by default. Only disabled if the team type |
| 24 | + * explicitly sets it to `false`. If the property is `undefined`, it is treated as enabled. |
| 25 | + * Only meaningful when `teamKey` is set. Ignored otherwise. |
| 26 | + * |
| 27 | + * @property {string} [platformSource] - Override where the platform value is read from. |
| 28 | + * - `undefined` (default): reads from `state.features` (the platform feature flags) |
| 29 | + * - `'settings'`: reads from `state.settings.features` instead (for features exposed |
| 30 | + * through the settings object rather than the feature flags system) |
| 31 | + * Only meaningful when `platformKey` is set. Ignored otherwise. |
| 32 | + * |
| 33 | + * @property {string} [dependsOn] - The `output` name of another feature that must be |
| 34 | + * enabled (combined check) for this feature to be enabled. If the referenced feature's |
| 35 | + * combined result is `false`, this feature is forced to `false`. |
| 36 | + * The dependency must appear earlier in the array to ensure it is computed first. |
| 37 | + * |
| 38 | + * @property {string} [dependsOnPlatform] - A platform feature key that must be enabled |
| 39 | + * for this feature to be enabled. Checked directly against the platform features object, |
| 40 | + * independent of whether the dependency has its own entry in FEATURE_CONFIGS. |
| 41 | + * |
| 42 | + * @property {string} [dependsOnPlatformSource] - Override where the `dependsOnPlatform` |
| 43 | + * value is read from, same semantics as `platformSource`. |
| 44 | + * Only meaningful when `dependsOnPlatform` is set. Ignored otherwise. |
| 45 | + * |
| 46 | + * @property {string} [dependsOnTeam] - A team feature key that must be enabled for this |
| 47 | + * feature to be enabled. Checked directly against the team type properties. |
| 48 | + * |
| 49 | + * @property {boolean} [dependsOnTeamOptOut] - Controls the default behavior of the |
| 50 | + * `dependsOnTeam` check, same semantics as `optOut`. |
| 51 | + * Only meaningful when `dependsOnTeam` is set. Ignored otherwise. |
| 52 | + * |
| 53 | + * Combination rules for the `{output}` property: |
| 54 | + * - If both `platformKey` and `teamKey` are set: `platform AND team` |
| 55 | + * - If only `platformKey` is set: `platform` only (platform-only feature) |
| 56 | + * - If only `teamKey` is set: `team` only (team-only feature) |
| 57 | + * - After the base combination, all `dependsOn*` gates are applied. If any |
| 58 | + * dependency check fails, the combined result is forced to `false`. |
| 59 | + * |
| 60 | + * At least one of `platformKey` or `teamKey` must be provided. |
| 61 | + * `dependsOn`, `dependsOnPlatform`, and `dependsOnTeam` can be used together. |
| 62 | + */ |
| 63 | + |
| 64 | +interface FeatureConfig { |
| 65 | + output: string |
| 66 | + platformKey?: string |
| 67 | + teamKey?: string |
| 68 | + optOut?: boolean |
| 69 | + platformSource?: 'settings' |
| 70 | + dependsOn?: string |
| 71 | + dependsOnPlatform?: string |
| 72 | + dependsOnPlatformSource?: 'settings' |
| 73 | + dependsOnTeam?: string |
| 74 | + dependsOnTeamOptOut?: boolean |
| 75 | +} |
| 76 | + |
| 77 | +interface PlatformState { |
| 78 | + features?: Record<string, boolean> |
| 79 | + settings?: { |
| 80 | + features?: Record<string, boolean> |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +interface TeamTypeProperties { |
| 85 | + features?: Record<string, boolean> |
| 86 | + enableAllFeatures?: boolean |
| 87 | +} |
| 88 | + |
| 89 | +interface Team { |
| 90 | + type?: { |
| 91 | + properties?: TeamTypeProperties |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +type FeatureChecks = Record<string, boolean> |
| 96 | + |
| 97 | +export const FEATURE_CONFIGS: FeatureConfig[] = [ |
| 98 | + { output: 'isSharedLibraryFeatureEnabled', platformKey: 'shared-library', teamKey: 'shared-library', optOut: true }, |
| 99 | + { output: 'isBlueprintsFeatureEnabled', platformKey: 'flowBlueprints', teamKey: 'flowBlueprints', optOut: true }, |
| 100 | + { output: 'isCustomCatalogsFeatureEnabled', platformKey: 'customCatalogs', teamKey: 'customCatalogs', optOut: true }, |
| 101 | + { output: 'isPrivateRegistryFeatureEnabled', platformKey: 'npm', teamKey: 'npm' }, |
| 102 | + { output: 'isStaticAssetsFeatureEnabled', platformKey: 'staticAssets', teamKey: 'staticAssets' }, |
| 103 | + { output: 'isHTTPBearerTokensFeatureEnabled', platformKey: 'httpBearerTokens', teamKey: 'teamHttpSecurity', platformSource: 'settings' }, |
| 104 | + { output: 'isBOMFeatureEnabled', platformKey: 'bom', teamKey: 'bom' }, |
| 105 | + { output: 'isTimelineFeatureEnabled', platformKey: 'projectHistory', teamKey: 'projectHistory' }, |
| 106 | + { output: 'isMqttBrokerFeatureEnabled', platformKey: 'teamBroker', teamKey: 'teamBroker' }, |
| 107 | + { output: 'isGitIntegrationFeatureEnabled', platformKey: 'gitIntegration', teamKey: 'gitIntegration' }, |
| 108 | + { output: 'isInstanceResourcesFeatureEnabled', platformKey: 'instanceResources', teamKey: 'instanceResources' }, |
| 109 | + { output: 'isTablesFeatureEnabled', platformKey: 'tables', teamKey: 'tables' }, |
| 110 | + { output: 'isAiFeatureEnabled', platformKey: 'ai', teamKey: 'ai' }, |
| 111 | + { |
| 112 | + output: 'isGeneratedSnapshotDescriptionFeatureEnabled', |
| 113 | + platformKey: 'generatedSnapshotDescription', |
| 114 | + teamKey: 'generatedSnapshotDescription', |
| 115 | + dependsOnPlatform: 'ai', |
| 116 | + dependsOnTeam: 'ai', |
| 117 | + dependsOnTeamOptOut: true |
| 118 | + }, |
| 119 | + { output: 'isApplicationsRBACFeatureEnabled', platformKey: 'rbacApplication', teamKey: 'rbacApplication' }, |
| 120 | + |
| 121 | + // Team-only |
| 122 | + { output: 'isDeviceGroupsFeatureEnabled', teamKey: 'deviceGroups' }, |
| 123 | + |
| 124 | + // Platform-only |
| 125 | + { output: 'isCertifiedNodesFeatureEnabled', platformKey: 'certifiedNodes' }, |
| 126 | + { output: 'isFlowFuseNodesFeatureEnabled', platformKey: 'ffNodes' }, |
| 127 | + { output: 'isExpertAssistantFeatureEnabled', platformKey: 'expertAssistant', dependsOnPlatform: 'ai' }, |
| 128 | + { output: 'isInstanceAutoStackUpdateFeatureEnabled', platformKey: 'autoStackUpdate' }, |
| 129 | + { output: 'isDevOpsPipelinesFeatureEnabled', platformKey: 'devops-pipelines' }, |
| 130 | + { output: 'isExternalMqttBrokerFeatureEnabled', platformKey: 'externalBroker' } |
| 131 | +] |
| 132 | + |
| 133 | +function isPlatformFeatureEnabled (state: PlatformState, platformKey: string, platformSource?: 'settings'): boolean { |
| 134 | + const source = platformSource === 'settings' ? state.settings?.features : state.features |
| 135 | + return !!source?.[platformKey] |
| 136 | +} |
| 137 | + |
| 138 | +function isTeamFeatureEnabled (team: Team | null | undefined, teamKey: string, optOut?: boolean): boolean { |
| 139 | + if (optOut) { |
| 140 | + const flag = team?.type?.properties?.features?.[teamKey] |
| 141 | + return (flag === undefined || !!flag) || !!team?.type?.properties?.enableAllFeatures |
| 142 | + } |
| 143 | + return !!team?.type?.properties?.features?.[teamKey] || !!team?.type?.properties?.enableAllFeatures |
| 144 | +} |
| 145 | + |
| 146 | +function applyDependencyGates ( |
| 147 | + checks: FeatureChecks, |
| 148 | + output: string, |
| 149 | + config: FeatureConfig, |
| 150 | + state: PlatformState, |
| 151 | + team: Team | null | undefined |
| 152 | +): void { |
| 153 | + if (config.dependsOn && !checks[config.dependsOn]) { |
| 154 | + checks[output] = false |
| 155 | + } |
| 156 | + if (config.dependsOnPlatform && !isPlatformFeatureEnabled(state, config.dependsOnPlatform, config.dependsOnPlatformSource)) { |
| 157 | + checks[output] = false |
| 158 | + } |
| 159 | + if (config.dependsOnTeam && !isTeamFeatureEnabled(team, config.dependsOnTeam, config.dependsOnTeamOptOut)) { |
| 160 | + checks[output] = false |
| 161 | + } |
| 162 | +} |
| 163 | + |
| 164 | +export function buildFeatureChecks (state: PlatformState, team: Team | null | undefined): FeatureChecks { |
| 165 | + const checks: FeatureChecks = {} |
| 166 | + |
| 167 | + for (const config of FEATURE_CONFIGS) { |
| 168 | + const { output, platformKey, teamKey, optOut, platformSource } = config |
| 169 | + const platformCheckKey = `${output}ForPlatform` |
| 170 | + const teamCheckKey = `${output}ForTeam` |
| 171 | + |
| 172 | + if (platformKey) { |
| 173 | + checks[platformCheckKey] = isPlatformFeatureEnabled(state, platformKey, platformSource) |
| 174 | + } |
| 175 | + |
| 176 | + if (teamKey) { |
| 177 | + checks[teamCheckKey] = isTeamFeatureEnabled(team, teamKey, optOut) |
| 178 | + } |
| 179 | + |
| 180 | + if (platformKey && teamKey) { |
| 181 | + checks[output] = checks[platformCheckKey] && checks[teamCheckKey] |
| 182 | + } else if (platformKey) { |
| 183 | + checks[output] = checks[platformCheckKey] |
| 184 | + } else if (teamKey) { |
| 185 | + checks[output] = checks[teamCheckKey] |
| 186 | + } |
| 187 | + |
| 188 | + applyDependencyGates(checks, output, config, state, team) |
| 189 | + } |
| 190 | + |
| 191 | + return checks |
| 192 | +} |
0 commit comments