core-api: refactor FeatureFlagApi to register flags instead of assigning

This commit is contained in:
Patrik Oldsberg
2020-10-28 17:36:50 +01:00
parent e64fc519b8
commit b986a73cfe
7 changed files with 53 additions and 47 deletions
@@ -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<FeatureFlagsApi> = createApiRef({
id: 'core.featureflags',
description: 'Used to toggle functionality in features across Backstage',
+12 -9
View File
@@ -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<AnyApiFactory>;
@@ -135,7 +135,8 @@ export class PrivateAppImpl implements BackstageApp {
getRoutes(): JSX.Element[] {
const routes = new Array<JSX.Element>();
const registeredFeatureFlags = new Array<FeatureFlagsRegistryItem>();
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(<Route path="/*" element={<NotFoundErrorPage />} />);
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);
}
+17 -14
View File
@@ -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',
}),
+12 -10
View File
@@ -17,7 +17,7 @@
import {
FeatureFlagState,
FeatureFlagsApi,
FeatureFlagsRegistryItem,
FeatureFlag,
} from '../apis/definitions';
/**
@@ -133,23 +133,20 @@ export class UserFlags extends Map<string, FeatureFlagState> {
* that plugins wish to register for use in Backstage.
*/
export class FeatureFlagsRegistry extends Array<FeatureFlagsRegistryItem> {
static from(entries: FeatureFlagsRegistryItem[]) {
export class FeatureFlagsRegistry extends Array<FeatureFlag> {
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>
)[]
): FeatureFlagsRegistryItem[] {
...entries: (FeatureFlag | ConcatArray<FeatureFlag>)[]
): 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<FeatureFlagsRegistryItem> {
* 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;
@@ -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,
@@ -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;
@@ -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;
};