[core/apis] replaced FeatureFlagsContext with FeatureFlags.registeredFlags

This commit is contained in:
Bilawal Hameed
2020-03-27 18:53:54 +01:00
parent a8f304dbfb
commit 6fa6373536
4 changed files with 8 additions and 244 deletions
+3 -8
View File
@@ -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} />;
}
}
+2 -131
View File
@@ -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 />));
});
});
+2 -100
View File
@@ -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',
+1 -5
View File
@@ -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';