diff --git a/packages/core/src/api/app/AppBuilder.tsx b/packages/core/src/api/app/AppBuilder.tsx index bd40565260..6964c45a26 100644 --- a/packages/core/src/api/app/AppBuilder.tsx +++ b/packages/core/src/api/app/AppBuilder.tsx @@ -19,7 +19,7 @@ import { Route, Switch, Redirect } from 'react-router-dom'; import { AppContextProvider } from './AppContext'; import { App } from './types'; import BackstagePlugin from '../plugin/Plugin'; -import { FeatureFlagsEntry, FeatureFlagsContextProvider } from './FeatureFlags'; +import { FeatureFlags, FeatureFlagsEntry } from './FeatureFlags'; import { IconComponent, SystemIcons, @@ -102,6 +102,8 @@ export default class AppBuilder { } } + FeatureFlags.registeredFeatureFlags = registeredFeatureFlags; + routes.push( , ); @@ -117,13 +119,6 @@ export default class AppBuilder { rendered = ; } - rendered = ( - - ); - return () => ; } } diff --git a/packages/core/src/api/app/FeatureFlags.test.tsx b/packages/core/src/api/app/FeatureFlags.test.tsx index b803b27b3a..cb5be32394 100644 --- a/packages/core/src/api/app/FeatureFlags.test.tsx +++ b/packages/core/src/api/app/FeatureFlags.test.tsx @@ -16,12 +16,7 @@ import React, { ReactNode, useContext, useEffect, useRef } from 'react'; import { render } from '@testing-library/react'; -import { - FeatureFlags, - FeatureFlagsEntry, - FeatureFlagsContext, - FeatureFlagsContextProvider, -} from './FeatureFlags'; +import { FeatureFlags } from './FeatureFlags'; import { FeatureFlagState } from '../apis/definitions/featureFlags'; function useRenderCount() { @@ -37,13 +32,7 @@ function withFeatureFlags( { name: 'feature-flag-two', pluginId: 'plugin-two' }, { name: 'feature-flag-three', pluginId: 'plugin-two' }, ]), -) { - return ( - - {children} - - ); -} +) {} describe('FeatureFlags', () => { beforeEach(() => { @@ -255,121 +244,3 @@ describe('FeatureFlags', () => { }); }); }); - -describe('FeatureFlagsContext', () => { - beforeEach(() => { - window.localStorage.clear(); - }); - - it('returns an empty set without the context', () => { - const Component = () => { - const { featureFlags } = useContext(FeatureFlagsContext); - expect(featureFlags).toEqual(new Set()); - return null; - }; - - render(); - }); - - it('returns a set of registered feature flags', () => { - expect.assertions(2); - - const mockFeatureFlags = new Set([ - { name: 'feature-flag-one', pluginId: 'plugin-one' }, - { name: 'feature-flag-two', pluginId: 'plugin-two' }, - ]); - - const Component = () => { - const { featureFlags } = useContext(FeatureFlagsContext); - expect(featureFlags).toEqual(mockFeatureFlags); - return null; - }; - - render(withFeatureFlags(, mockFeatureFlags)); - }); - - it('returns a set of user enabled feature flags', () => { - expect.assertions(1); - - window.localStorage.setItem( - 'featureFlags', - JSON.stringify({ - 'feature-flag-one': true, - 'feature-flag-three': true, - }), - ); - - const Component = () => { - const { enabledFeatureFlags } = useContext(FeatureFlagsContext); - - if (enabledFeatureFlags.size > 0) { - expect(enabledFeatureFlags).toEqual( - new Set(['feature-flag-one', 'feature-flag-three']), - ); - } - - return null; - }; - - render(withFeatureFlags()); - }); - - it('correctly re-renders when calling refreshEnabledFeatureFlags', () => { - // First is the initial context - // Second is the context with the state from FeatureFlagsContextProvider - // Third is from calling refreshEnabledFeatureFlags - expect.assertions(3); - - const FirstRender = ({ context: { enabledFeatureFlags } }) => { - useEffect(() => { - expect(enabledFeatureFlags).toEqual(new Set()); - }, []); - return null; - }; - - const SecondRender = ({ - context: { enabledFeatureFlags, refreshEnabledFeatureFlags }, - }) => { - useEffect(() => { - expect(enabledFeatureFlags).toEqual(new Set()); - - // Change localStorage - window.localStorage.setItem( - 'featureFlags', - JSON.stringify({ - 'feature-flag-one': true, - 'feature-flag-three': true, - }), - ); - - // Refresh - refreshEnabledFeatureFlags(); - }, []); - - return null; - }; - - const ThirdRender = ({ context: { enabledFeatureFlags } }) => { - useEffect(() => { - expect(enabledFeatureFlags).toEqual( - new Set(['feature-flag-one', 'feature-flag-three']), - ); - }, []); - - return null; - }; - - const Component = () => { - const context = useContext(FeatureFlagsContext); - const renderCount = useRenderCount(); - - if (renderCount === 0) return ; - if (renderCount === 1) return ; - if (renderCount === 2) return ; - - return null; - }; - - render(withFeatureFlags()); - }); -}); diff --git a/packages/core/src/api/app/FeatureFlags.tsx b/packages/core/src/api/app/FeatureFlags.tsx index 4f2b7da7d0..172daeb878 100644 --- a/packages/core/src/api/app/FeatureFlags.tsx +++ b/packages/core/src/api/app/FeatureFlags.tsx @@ -14,78 +14,17 @@ * limitations under the License. */ -import React, { - ReactNode, - createContext, - useContext, - useState, - useEffect, - FC, -} from 'react'; import { FeatureFlagName } from '../plugin/types'; import { FeatureFlagState, FeatureFlagsApi, } from '../apis/definitions/featureFlags'; -/** - * Create a shared React context for Feature Flags. - * - * This will be used to propagate all available feature flags to - * Backstage components. This enables viewing all of the available flags. - */ export interface FeatureFlagsEntry { pluginId: string; name: FeatureFlagName; } -export interface IFeatureFlagsContext { - featureFlags: Set; - enabledFeatureFlags: Set; - refreshEnabledFeatureFlags: () => void; -} - -export const FeatureFlagsContext = createContext({ - featureFlags: new Set(), - enabledFeatureFlags: new Set(), - refreshEnabledFeatureFlags: () => { - throw new Error( - 'The refreshEnabledFeatureFlags method is not implemented as it is not called from within the FeatureFlagsContext context within React. ' + - 'See the Backstage documentation for examples on how to use the Feature Flags API.', - ); - }, -}); - -export const FeatureFlagsContextProvider: FC<{ - featureFlags: Set; - children: ReactNode; -}> = ({ featureFlags, children }) => { - const [enabledFeatureFlags, setEnabledFeatureFlags] = useState< - Set - >(new Set()); - - const refreshEnabledFeatureFlags = () => { - // eslint-disable-next-line no-use-before-define - setEnabledFeatureFlags(FeatureFlags.getEnabledFeatureFlags()); - }; - - // Initially populate our setEnabledFeatureFlags - useEffect(() => { - refreshEnabledFeatureFlags(); - }, []); - - return ( - - ); -}; - /** * Create the FeatureFlags implementation based on the API. */ @@ -95,46 +34,9 @@ export const FeatureFlagsContextProvider: FC<{ class FeatureFlagsImpl implements FeatureFlagsApi { private readonly localStorageKey = 'featureFlags'; - private get( - enabledFeatureFlags: Set, - name: FeatureFlagName, - ): FeatureFlagState { - if (enabledFeatureFlags.has(name)) { - return FeatureFlagState.Enabled; - } + public constructor(public registeredFeatureFlags: FeatureFlagsEntry[] = []) {} - return FeatureFlagState.NotEnabled; - } - - private set(name: FeatureFlagName, state: FeatureFlagState): void { - const errors = this.checkFeatureFlagNameErrors(name); - const flags = this.getEnabledFeatureFlags(); - - if (errors.length > 0) { - throw new Error(errors[0]); - } - - if (state === FeatureFlagState.Enabled) { - flags.add(name); - } else if (state === FeatureFlagState.NotEnabled) { - flags.delete(name); - } else { - throw new Error( - 'The `state` argument requires a recognized value from the FeatureFlagState enum. ' + - 'Please check the Backstage documentation to see all the available options.' + - 'Example values: FeatureFlagState.NotEnabled, FeatureFlagState.Enabled', - ); - } - - window.localStorage.setItem( - this.localStorageKey, - JSON.stringify( - [...flags].reduce((list, flag) => ({ ...list, [flag]: true }), {}), - ), - ); - } - - getEnabledFeatureFlags(): Set { + private getUserEnabledFeatureFlags(): Set { if (!('localStorage' in window)) { throw new Error( 'Feature Flags are not supported on browsers without the Local Storage API', diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index 4c77e94984..e60a7efa18 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -16,9 +16,5 @@ export * from './api'; export * from './apis'; -export { - FeatureFlags, - FeatureFlagsContext, - FeatureFlagsContextProvider, -} from './app/FeatureFlags'; +export { FeatureFlags } from './app/FeatureFlags'; export { useApp } from './app/AppContext';