[core/apis] replaced FeatureFlagsContext with FeatureFlags.registeredFlags
This commit is contained in:
@@ -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(
|
||||
<Route key="login" path="/login" component={LoginPage} exact />,
|
||||
);
|
||||
@@ -117,13 +119,6 @@ export default class AppBuilder {
|
||||
rendered = <ApiProvider apis={this.apis} children={rendered} />;
|
||||
}
|
||||
|
||||
rendered = (
|
||||
<FeatureFlagsContextProvider
|
||||
featureFlags={registeredFeatureFlags}
|
||||
children={rendered}
|
||||
/>
|
||||
);
|
||||
|
||||
return () => <AppContextProvider app={app} children={rendered} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<FeatureFlagsContextProvider featureFlags={featureFlags}>
|
||||
{children}
|
||||
</FeatureFlagsContextProvider>
|
||||
);
|
||||
}
|
||||
) {}
|
||||
|
||||
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(<Component />);
|
||||
});
|
||||
|
||||
it('returns a set of registered feature flags', () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const mockFeatureFlags = new Set<FeatureFlagsEntry>([
|
||||
{ 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(<Component />, 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(<Component />));
|
||||
});
|
||||
|
||||
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 <FirstRender context={context} />;
|
||||
if (renderCount === 1) return <SecondRender context={context} />;
|
||||
if (renderCount === 2) return <ThirdRender context={context} />;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
render(withFeatureFlags(<Component />));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<FeatureFlagsEntry>;
|
||||
enabledFeatureFlags: Set<FeatureFlagName>;
|
||||
refreshEnabledFeatureFlags: () => void;
|
||||
}
|
||||
|
||||
export const FeatureFlagsContext = createContext<IFeatureFlagsContext>({
|
||||
featureFlags: new Set<FeatureFlagsEntry>(),
|
||||
enabledFeatureFlags: new Set<FeatureFlagName>(),
|
||||
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<FeatureFlagsEntry>;
|
||||
children: ReactNode;
|
||||
}> = ({ featureFlags, children }) => {
|
||||
const [enabledFeatureFlags, setEnabledFeatureFlags] = useState<
|
||||
Set<FeatureFlagName>
|
||||
>(new Set<FeatureFlagName>());
|
||||
|
||||
const refreshEnabledFeatureFlags = () => {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
setEnabledFeatureFlags(FeatureFlags.getEnabledFeatureFlags());
|
||||
};
|
||||
|
||||
// Initially populate our setEnabledFeatureFlags
|
||||
useEffect(() => {
|
||||
refreshEnabledFeatureFlags();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FeatureFlagsContext.Provider
|
||||
value={{
|
||||
featureFlags,
|
||||
enabledFeatureFlags,
|
||||
refreshEnabledFeatureFlags,
|
||||
}}
|
||||
children={children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<string>,
|
||||
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<FeatureFlagName> {
|
||||
private getUserEnabledFeatureFlags(): Set<FeatureFlagName> {
|
||||
if (!('localStorage' in window)) {
|
||||
throw new Error(
|
||||
'Feature Flags are not supported on browsers without the Local Storage API',
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user