[core/apis] switch to useFeatureFlag React Hook

This commit is contained in:
Bilawal Hameed
2020-03-27 12:49:48 +01:00
parent 5157904031
commit c1f5b2abc9
4 changed files with 126 additions and 54 deletions
@@ -36,24 +36,27 @@ export enum FeatureFlagState {
export type FeatureFlagsApi = {
/**
* Check the feature flag name convention. Used in the
* `registerFeatureFlag` method as well as in the `set` method.
* Get a list of the user's currently enabled feature flags.
* Reads directly from window.localStorage Browser API.
*
* @returns string[] enabledFeatureFlags List of feature flags enabled by the current user
*/
getEnabledFeatureFlags(): Set<FeatureFlagName>;
/**
* Check the feature flag name convention.
* Used in the `registerFeatureFlag` method as well as in the `set` method.
*
* @returns string[] errors List of errors as string. Empty array if no errors.
*/
checkFeatureFlagNameErrors(name: FeatureFlagName): string[];
/**
* Get the current user's status of a Feature Flag
*
* @returns bool True if the current user has enabled the feature flag
* Use Feature Flags as a React Hook.
*/
get(name: FeatureFlagName): FeatureFlagState;
/**
* Set the state of a Feature Flag
*/
set(name: FeatureFlagName, state: FeatureFlagState): void;
useFeatureFlag(
name: FeatureFlagName,
): [FeatureFlagState, (state: FeatureFlagState) => void];
};
export const featureFlagsApiRef = new ApiRef<FeatureFlagsApi>({
+2 -2
View File
@@ -63,7 +63,7 @@ export default class AppBuilder {
const app = new AppImpl(this.systemIcons);
const routes = new Array<JSX.Element>();
const registeredFeatureFlags = new Array<FeatureFlagsEntry>();
const registeredFeatureFlags = new Set<FeatureFlagsEntry>();
for (const plugin of this.plugins.values()) {
for (const output of plugin.output()) {
@@ -90,7 +90,7 @@ export default class AppBuilder {
break;
}
case 'feature-flag': {
registeredFeatureFlags.push({
registeredFeatureFlags.add({
pluginId: plugin.getId(),
name: output.name,
});
+101 -31
View File
@@ -19,6 +19,7 @@ import React, {
createContext,
useContext,
useState,
useEffect,
FC,
} from 'react';
import { FeatureFlagName } from '../plugin/types';
@@ -36,26 +37,50 @@ import {
export interface FeatureFlagsEntry {
pluginId: string;
name: FeatureFlagName;
userEnabled: boolean;
}
export interface IFeatureFlagsContext {
featureFlags: FeatureFlagsEntry[];
featureFlags: Set<FeatureFlagsEntry>;
enabledFeatureFlags: Set<FeatureFlagName>;
refreshEnabledFeatureFlags: () => void;
}
export const FeatureFlagsContext = createContext<IFeatureFlagsContext>({
featureFlags: [],
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: FeatureFlagsEntry[];
featureFlags: Set<FeatureFlagsEntry>;
children: ReactNode;
}> = ({ featureFlags, children }) => {
const [userEnabledFlags, setUserEnabledFlags] = useState<string[]>([]);
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, userEnabledFlags, setUserEnabledFlags }}
value={{
featureFlags,
enabledFeatureFlags,
refreshEnabledFeatureFlags,
}}
children={children}
/>
);
@@ -70,7 +95,46 @@ export const FeatureFlagsContextProvider: FC<{
class FeatureFlagsImpl implements FeatureFlagsApi {
private readonly localStorageKey = 'featureFlags';
private getUserEnabledFeatureFlags(): Set<FeatureFlagName> {
private get(
enabledFeatureFlags: Set<string>,
name: FeatureFlagName,
): FeatureFlagState {
if (enabledFeatureFlags.has(name)) {
return FeatureFlagState.Enabled;
}
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> {
if (!('localStorage' in window)) {
throw new Error(
'Feature Flags are not supported on browsers without the Local Storage API',
@@ -114,40 +178,46 @@ class FeatureFlagsImpl implements FeatureFlagsApi {
return errors;
}
get(name: FeatureFlagName): FeatureFlagState {
if (this.getUserEnabledFeatureFlags().has(name)) {
return FeatureFlagState.Enabled;
useFeatureFlag(
name: FeatureFlagName,
): [FeatureFlagState, (state: FeatureFlagState) => void] {
// Check for context
const context = useContext(FeatureFlagsContext);
if (!context) {
throw new Error(
'No FeatureFlagsContext found. ' +
'Please use this React Hook in the context of your <App />',
);
}
return FeatureFlagState.NotEnabled;
}
set(name: FeatureFlagName, state: FeatureFlagState): void {
const errors = this.checkFeatureFlagNameErrors(name);
const flags = this.getUserEnabledFeatureFlags();
// Check for errors
// eslint-disable-next-line no-use-before-define
const errors = FeatureFlags.checkFeatureFlagNameErrors(name);
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 {
// Check if the feature flag is registered
const allFlagNames = [...context.featureFlags].map(flag => flag.name);
if (!allFlagNames.includes(name)) {
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',
`The '${name}' feature flag is not registered by any plugin. ` +
`See the 'registerFeatureFlag' method in the Plugin API (or in your plugin.ts file) on how to register Feature Flags.`,
);
}
window.localStorage.setItem(
this.localStorageKey,
JSON.stringify(
[...flags].reduce((list, flag) => ({ ...list, [flag]: true }), {}),
),
);
// eslint-disable-next-line no-use-before-define
const currentState = FeatureFlags.get(context.enabledFeatureFlags, name);
const setState = (state: FeatureFlagState): void => {
// Set the value
// eslint-disable-next-line no-use-before-define
FeatureFlags.set(name, state);
// Now update the global state
context.refreshEnabledFeatureFlags();
};
return [currentState, setState];
}
}
@@ -19,15 +19,15 @@ import { Button } from '@material-ui/core';
import { FeatureFlagState, featureFlagsApiRef, useApi } from '@backstage/core';
const ToggleFeatureFlagButton: FC<{}> = () => {
const featureFlagsApi = useApi(featureFlagsApiRef);
const { useFeatureFlag } = useApi(featureFlagsApiRef);
const [flagState, setFlagState] = useFeatureFlag('enable-welcome-box');
const handleClick = () => {
const isEnabled = featureFlagsApi.get('enable-welcome-box');
featureFlagsApi.set(
'enable-welcome-box',
isEnabled ? FeatureFlagState.NotEnabled : FeatureFlagState.Enabled,
);
window.location.reload();
if (flagState === FeatureFlagState.Enabled) {
setFlagState(FeatureFlagState.NotEnabled);
} else {
setFlagState(FeatureFlagState.Enabled);
}
};
return (
@@ -37,10 +37,9 @@ const ToggleFeatureFlagButton: FC<{}> = () => {
onClick={handleClick}
data-testid="button-switch-feature-flag-state"
>
{featureFlagsApi.get('enable-welcome-box') ===
FeatureFlagState.NotEnabled &&
{flagState === FeatureFlagState.NotEnabled &&
'Enable "enable-welcome-box" feature flag'}
{featureFlagsApi.get('enable-welcome-box') === FeatureFlagState.Enabled &&
{flagState === FeatureFlagState.Enabled &&
'Disable "enable-welcome-box" feature flag'}
</Button>
);