diff --git a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts index 26620aea29..0f6e83b2ed 100644 --- a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts @@ -34,11 +34,17 @@ export enum FeatureFlagState { On = 1, } +export interface FeatureFlag { + name: string; + pluginId: string; +} + export interface FeatureFlagsApi { /** - * Store a list of registered feature flags. + * Registers a new feature flag. Once a feature flag has been registered it + * can be toggled by users, and read back to enable or disable features. */ - registeredFeatureFlags: FeatureFlagsRegistryItem[]; + registerFlag(flag: FeatureFlag): void; /** * Get a list of all feature flags from the current user. @@ -51,11 +57,6 @@ export interface FeatureFlagsApi { getRegisteredFlags(): FeatureFlagsRegistry; } -export interface FeatureFlagsRegistryItem { - pluginId: string; - name: string; -} - export const featureFlagsApiRef: ApiRef = createApiRef({ id: 'core.featureflags', description: 'Used to toggle functionality in features across Backstage', diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 0588cd819c..45496b9b8a 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -35,7 +35,6 @@ import { AppThemeApi, ConfigApi, identityApiRef, - FeatureFlagsRegistryItem, } from '../apis/definitions'; import { AppThemeProvider } from './AppThemeProvider'; @@ -55,6 +54,7 @@ import { import { useAsync } from 'react-use'; import { AppIdentity } from './AppIdentity'; import { ApiResolver, ApiFactoryRegistry } from '../apis/system'; +import { FeatureFlags } from './FeatureFlags'; type FullAppOptions = { apis: Iterable; @@ -135,7 +135,8 @@ export class PrivateAppImpl implements BackstageApp { getRoutes(): JSX.Element[] { const routes = new Array(); - const registeredFeatureFlags = new Array(); + + const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!; const { NotFoundErrorPage } = this.components; @@ -171,9 +172,9 @@ export class PrivateAppImpl implements BackstageApp { break; } case 'feature-flag': { - registeredFeatureFlags.push({ - pluginId: plugin.getId(), + featureFlagsApi.registerFlag({ name: output.name, + pluginId: plugin.getId(), }); break; } @@ -183,11 +184,6 @@ export class PrivateAppImpl implements BackstageApp { } } - const featureFlags = this.getApiHolder().get(featureFlagsApiRef); - if (featureFlags) { - featureFlags.registeredFeatureFlags = registeredFeatureFlags; - } - routes.push(} />); return routes; @@ -319,6 +315,13 @@ export class PrivateAppImpl implements BackstageApp { factory: () => this.identityApi, }); + // It's possible to replace the feature flag API, but since we must have at least + // one implementation we add it here directly instead of through the defaultApis. + registry.register('default', { + api: featureFlagsApiRef, + deps: {}, + factory: () => new FeatureFlags(), + }); for (const factory of this.defaultApis) { registry.register('default', factory); } diff --git a/packages/core-api/src/app/FeatureFlags.test.tsx b/packages/core-api/src/app/FeatureFlags.test.tsx index 99974f9b1d..66dc2593f6 100644 --- a/packages/core-api/src/app/FeatureFlags.test.tsx +++ b/packages/core-api/src/app/FeatureFlags.test.tsx @@ -125,15 +125,22 @@ describe('FeatureFlags', () => { beforeEach(() => { featureFlags = new FeatureFlagsImpl(); - featureFlags.registeredFeatureFlags = [ - { name: 'registered-flag-1', pluginId: 'plugin-one' }, - { name: 'registered-flag-2', pluginId: 'plugin-one' }, - { name: 'registered-flag-3', pluginId: 'plugin-two' }, - ]; + featureFlags.registerFlag({ + name: 'registered-flag-1', + pluginId: 'plugin-one', + }); + featureFlags.registerFlag({ + name: 'registered-flag-2', + pluginId: 'plugin-one', + }); + featureFlags.registerFlag({ + name: 'registered-flag-3', + pluginId: 'plugin-two', + }); }); it('should return an empty list', () => { - featureFlags.registeredFeatureFlags = []; + featureFlags = new FeatureFlagsImpl(); expect(featureFlags.getRegisteredFlags().toObject()).toEqual([]); }); @@ -203,9 +210,8 @@ describe('FeatureFlags', () => { }); it('throws an error if length is less than three characters', () => { - const flags = featureFlags.getRegisteredFlags(); expect(() => - flags.push({ + featureFlags.registerFlag({ name: 'ab', pluginId: 'plugin-three', }), @@ -213,9 +219,8 @@ describe('FeatureFlags', () => { }); it('throws an error if length is greater than 150 characters', () => { - const flags = featureFlags.getRegisteredFlags(); expect(() => - flags.push({ + featureFlags.registerFlag({ name: 'loremipsumdolorsitametconsecteturadipiscingelitnuncvitaeportaexaullamcorperturpismaurisutmattisnequemorbisediaculisauguevivamuspulvinarcursuseratblandithendreritquisqueuttinciduntmagnavestibulumblanditaugueat', pluginId: 'plugin-three', @@ -224,9 +229,8 @@ describe('FeatureFlags', () => { }); it('throws an error if name does not start with a lowercase letter', () => { - const flags = featureFlags.getRegisteredFlags(); expect(() => - flags.push({ + featureFlags.registerFlag({ name: '123456789', pluginId: 'plugin-three', }), @@ -234,9 +238,8 @@ describe('FeatureFlags', () => { }); it('throws an error if name contains characters other than lowercase letters, numbers and hyphens', () => { - const flags = featureFlags.getRegisteredFlags(); expect(() => - flags.push({ + featureFlags.registerFlag({ name: 'Invalid_Feature_Flag', pluginId: 'plugin-three', }), diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx index 115e46802b..22e6a89329 100644 --- a/packages/core-api/src/app/FeatureFlags.tsx +++ b/packages/core-api/src/app/FeatureFlags.tsx @@ -17,7 +17,7 @@ import { FeatureFlagState, FeatureFlagsApi, - FeatureFlagsRegistryItem, + FeatureFlag, } from '../apis/definitions'; /** @@ -133,23 +133,20 @@ export class UserFlags extends Map { * that plugins wish to register for use in Backstage. */ -export class FeatureFlagsRegistry extends Array { - static from(entries: FeatureFlagsRegistryItem[]) { +export class FeatureFlagsRegistry extends Array { + static from(entries: FeatureFlag[]) { Array.from(entries).forEach(entry => validateFlagName(entry.name)); return new FeatureFlagsRegistry(...entries); } - push(...entries: FeatureFlagsRegistryItem[]): number { + push(...entries: FeatureFlag[]): number { Array.from(entries).forEach(entry => validateFlagName(entry.name)); return super.push(...entries); } concat( - ...entries: ( - | FeatureFlagsRegistryItem - | ConcatArray - )[] - ): FeatureFlagsRegistryItem[] { + ...entries: (FeatureFlag | ConcatArray)[] + ): FeatureFlag[] { const _concat = super.concat(...entries); Array.from(_concat).forEach(entry => validateFlagName(entry.name)); return _concat; @@ -172,9 +169,14 @@ export class FeatureFlagsRegistry extends Array { * Create the FeatureFlags implementation based on the API. */ export class FeatureFlags implements FeatureFlagsApi { - public registeredFeatureFlags: FeatureFlagsRegistryItem[] = []; + private registeredFeatureFlags: FeatureFlag[] = []; private userFlags: UserFlags | undefined; + registerFlag(flag: FeatureFlag) { + validateFlagName(flag.name); + this.registeredFeatureFlags.push(flag); + } + getFlags(): UserFlags { if (!this.userFlags) this.userFlags = UserFlags.load(); return this.userFlags; diff --git a/packages/core/src/api-wrappers/defaultApis.ts b/packages/core/src/api-wrappers/defaultApis.ts index e735e4d371..d0a777dc94 100644 --- a/packages/core/src/api-wrappers/defaultApis.ts +++ b/packages/core/src/api-wrappers/defaultApis.ts @@ -20,8 +20,6 @@ import { AlertApiForwarder, ErrorApiForwarder, ErrorAlerter, - featureFlagsApiRef, - FeatureFlags, discoveryApiRef, GoogleAuth, GithubAuth, @@ -69,7 +67,6 @@ export const defaultApis = [ deps: { errorApi: errorApiRef }, factory: ({ errorApi }) => WebStorage.create({ errorApi }), }), - createApiFactory(featureFlagsApiRef, new FeatureFlags()), createApiFactory(oauthRequestApiRef, new OAuthRequestManager()), createApiFactory({ api: googleAuthApiRef, diff --git a/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx index 851f427b62..3a69fbfc01 100644 --- a/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx @@ -17,7 +17,7 @@ import React, { useCallback, useState } from 'react'; import { featureFlagsApiRef, - FeatureFlagsRegistryItem, + FeatureFlag, FeatureFlagState, InfoCard, useApi, @@ -30,7 +30,7 @@ export const FeatureFlags = () => { const featureFlagsApi = useApi(featureFlagsApiRef); const featureFlags = featureFlagsApi.getRegisteredFlags(); const initialFlagState = featureFlags.reduce( - (result, featureFlag: FeatureFlagsRegistryItem) => { + (result, featureFlag: FeatureFlag) => { const state = featureFlagsApi.getFlags().get(featureFlag.name); result[featureFlag.name] = state; diff --git a/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx b/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx index 460a15f56b..603f63ef73 100644 --- a/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx @@ -22,10 +22,10 @@ import { Switch, Tooltip, } from '@material-ui/core'; -import { FeatureFlagsRegistryItem } from '@backstage/core'; +import { FeatureFlag } from '@backstage/core'; type Props = { - flag: FeatureFlagsRegistryItem; + flag: FeatureFlag; enabled: boolean; toggleHandler: Function; };