core-api: refactor FeatureFlagApi to register flags instead of assigning
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user