From e64fc519b88a537946676108aaaf04ebd704ed2b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 16:49:09 +0100 Subject: [PATCH 1/8] core-api: remove FeatureFlagName type --- docs/reference/createPlugin-feature-flags.md | 2 +- .../src/apis/definitions/FeatureFlagsApi.ts | 3 +-- packages/core-api/src/app/FeatureFlags.tsx | 13 ++++++------- packages/core-api/src/plugin/types.ts | 6 ++---- .../src/components/FeatureFlags/FeatureFlags.tsx | 7 +++---- 5 files changed, 13 insertions(+), 18 deletions(-) diff --git a/docs/reference/createPlugin-feature-flags.md b/docs/reference/createPlugin-feature-flags.md index ebb8a6b503..097294e55f 100644 --- a/docs/reference/createPlugin-feature-flags.md +++ b/docs/reference/createPlugin-feature-flags.md @@ -10,7 +10,7 @@ can use this to split out logic in your code for manual A/B testing, etc. ```typescript export type FeatureFlagsHooks = { - register(name: FeatureFlagName): void; + register(name: string): void; }; ``` diff --git a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts index 6e8c8cd8fc..26620aea29 100644 --- a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts @@ -16,7 +16,6 @@ import { ApiRef, createApiRef } from '../system'; import { UserFlags, FeatureFlagsRegistry } from '../../app/FeatureFlags'; -import { FeatureFlagName } from '../../plugin'; /** * The feature flags API is used to toggle functionality to users across plugins and Backstage. @@ -54,7 +53,7 @@ export interface FeatureFlagsApi { export interface FeatureFlagsRegistryItem { pluginId: string; - name: FeatureFlagName; + name: string; } export const featureFlagsApiRef: ApiRef = createApiRef({ diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx index 7a69fa5456..115e46802b 100644 --- a/packages/core-api/src/app/FeatureFlags.tsx +++ b/packages/core-api/src/app/FeatureFlags.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { FeatureFlagName } from '../plugin/types'; import { FeatureFlagState, FeatureFlagsApi, @@ -32,7 +31,7 @@ export function validateBrowserCompat(): void { } } -export function validateFlagName(name: FeatureFlagName): void { +export function validateFlagName(name: string): void { if (name.length < 3) { throw new Error( `The '${name}' feature flag must have a minimum length of three characters.`, @@ -60,7 +59,7 @@ export function validateFlagName(name: FeatureFlagName): void { * can use this to retrieve, add, edit, delete, clear and save the user's * feature flags to the local browser for persisted storage. */ -export class UserFlags extends Map { +export class UserFlags extends Map { static load(): UserFlags { validateBrowserCompat(); @@ -73,18 +72,18 @@ export class UserFlags extends Map { } } - get(name: FeatureFlagName): FeatureFlagState { + get(name: string): FeatureFlagState { return super.get(name) || FeatureFlagState.Off; } - set(name: FeatureFlagName, state: FeatureFlagState): this { + set(name: string, state: FeatureFlagState): this { validateFlagName(name); const output = super.set(name, state); this.save(); return output; } - toggle(name: FeatureFlagName): FeatureFlagState { + toggle(name: string): FeatureFlagState { if (super.get(name) === FeatureFlagState.On) { super.set(name, FeatureFlagState.Off); } else { @@ -93,7 +92,7 @@ export class UserFlags extends Map { return super.get(name) || FeatureFlagState.Off; } - delete(name: FeatureFlagName): boolean { + delete(name: string): boolean { const output = super.delete(name); this.save(); return output; diff --git a/packages/core-api/src/plugin/types.ts b/packages/core-api/src/plugin/types.ts index 76c05f7b04..427d3d539a 100644 --- a/packages/core-api/src/plugin/types.ts +++ b/packages/core-api/src/plugin/types.ts @@ -54,11 +54,9 @@ export type LegacyRedirectRouteOutput = { options?: RouteOptions; }; -export type FeatureFlagName = string; - export type FeatureFlagOutput = { type: 'feature-flag'; - name: FeatureFlagName; + name: string; }; export type PluginOutput = @@ -103,5 +101,5 @@ export type RouterHooks = { }; export type FeatureFlagsHooks = { - register(name: FeatureFlagName): void; + register(name: string): void; }; diff --git a/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx index e0f4d6f40e..851f427b62 100644 --- a/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx @@ -16,7 +16,6 @@ import React, { useCallback, useState } from 'react'; import { - FeatureFlagName, featureFlagsApiRef, FeatureFlagsRegistryItem, FeatureFlagState, @@ -37,15 +36,15 @@ export const FeatureFlags = () => { result[featureFlag.name] = state; return result; }, - {} as Record, + {} as Record, ); - const [state, setState] = useState>( + const [state, setState] = useState>( initialFlagState, ); const toggleFlag = useCallback( - (flagName: FeatureFlagName) => { + (flagName: string) => { const newState = featureFlagsApi.getFlags().toggle(flagName); setState(prevState => ({ From b986a73cfe6cdb3da7d4c29fb18e75341e87a23c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 17:36:50 +0100 Subject: [PATCH 2/8] core-api: refactor FeatureFlagApi to register flags instead of assigning --- .../src/apis/definitions/FeatureFlagsApi.ts | 15 ++++----- packages/core-api/src/app/App.tsx | 21 +++++++------ .../core-api/src/app/FeatureFlags.test.tsx | 31 ++++++++++--------- packages/core-api/src/app/FeatureFlags.tsx | 22 +++++++------ packages/core/src/api-wrappers/defaultApis.ts | 3 -- .../components/FeatureFlags/FeatureFlags.tsx | 4 +-- .../FeatureFlags/FeatureFlagsItem.tsx | 4 +-- 7 files changed, 53 insertions(+), 47 deletions(-) diff --git a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts index 26620aea29..0f6e83b2ed 100644 --- a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts @@ -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 = createApiRef({ id: 'core.featureflags', description: 'Used to toggle functionality in features across Backstage', diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 0588cd819c..45496b9b8a 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -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; @@ -135,7 +135,8 @@ export class PrivateAppImpl implements BackstageApp { getRoutes(): JSX.Element[] { const routes = new Array(); - const registeredFeatureFlags = new Array(); + + 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(} />); 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); } diff --git a/packages/core-api/src/app/FeatureFlags.test.tsx b/packages/core-api/src/app/FeatureFlags.test.tsx index 99974f9b1d..66dc2593f6 100644 --- a/packages/core-api/src/app/FeatureFlags.test.tsx +++ b/packages/core-api/src/app/FeatureFlags.test.tsx @@ -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', }), diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx index 115e46802b..22e6a89329 100644 --- a/packages/core-api/src/app/FeatureFlags.tsx +++ b/packages/core-api/src/app/FeatureFlags.tsx @@ -17,7 +17,7 @@ import { FeatureFlagState, FeatureFlagsApi, - FeatureFlagsRegistryItem, + FeatureFlag, } from '../apis/definitions'; /** @@ -133,23 +133,20 @@ export class UserFlags extends Map { * that plugins wish to register for use in Backstage. */ -export class FeatureFlagsRegistry extends Array { - static from(entries: FeatureFlagsRegistryItem[]) { +export class FeatureFlagsRegistry extends Array { + 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[] { + ...entries: (FeatureFlag | ConcatArray)[] + ): 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 { * 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; diff --git a/packages/core/src/api-wrappers/defaultApis.ts b/packages/core/src/api-wrappers/defaultApis.ts index e735e4d371..d0a777dc94 100644 --- a/packages/core/src/api-wrappers/defaultApis.ts +++ b/packages/core/src/api-wrappers/defaultApis.ts @@ -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, diff --git a/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx index 851f427b62..3a69fbfc01 100644 --- a/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx @@ -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; diff --git a/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx b/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx index 460a15f56b..603f63ef73 100644 --- a/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx @@ -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; }; From a9ea34404e301ea1587e6a2e0908226a0a05bb1e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 17:43:43 +0100 Subject: [PATCH 3/8] core-api: remove FeatureFlagsRegistry class --- .../src/apis/definitions/FeatureFlagsApi.ts | 12 ++--- .../core-api/src/app/FeatureFlags.test.tsx | 6 +-- packages/core-api/src/app/FeatureFlags.tsx | 47 ++----------------- 3 files changed, 13 insertions(+), 52 deletions(-) diff --git a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts index 0f6e83b2ed..76a1e4f9ba 100644 --- a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts @@ -15,7 +15,7 @@ */ import { ApiRef, createApiRef } from '../system'; -import { UserFlags, FeatureFlagsRegistry } from '../../app/FeatureFlags'; +import { UserFlags } from '../../app/FeatureFlags'; /** * The feature flags API is used to toggle functionality to users across plugins and Backstage. @@ -46,15 +46,15 @@ export interface FeatureFlagsApi { */ registerFlag(flag: FeatureFlag): void; + /** + * Get a list of all registered flags. + */ + getRegisteredFlags(): FeatureFlag[]; + /** * Get a list of all feature flags from the current user. */ getFlags(): UserFlags; - - /** - * Get a list of all registered flags. - */ - getRegisteredFlags(): FeatureFlagsRegistry; } export const featureFlagsApiRef: ApiRef = createApiRef({ diff --git a/packages/core-api/src/app/FeatureFlags.test.tsx b/packages/core-api/src/app/FeatureFlags.test.tsx index 66dc2593f6..c04e60b069 100644 --- a/packages/core-api/src/app/FeatureFlags.test.tsx +++ b/packages/core-api/src/app/FeatureFlags.test.tsx @@ -141,11 +141,11 @@ describe('FeatureFlags', () => { it('should return an empty list', () => { featureFlags = new FeatureFlagsImpl(); - expect(featureFlags.getRegisteredFlags().toObject()).toEqual([]); + expect(featureFlags.getRegisteredFlags()).toEqual([]); }); it('should return an valid list', () => { - expect(featureFlags.getRegisteredFlags().toObject()).toMatchObject([ + expect(featureFlags.getRegisteredFlags()).toEqual([ { name: 'registered-flag-1', pluginId: 'plugin-one' }, { name: 'registered-flag-2', pluginId: 'plugin-one' }, { name: 'registered-flag-3', pluginId: 'plugin-two' }, @@ -179,7 +179,7 @@ describe('FeatureFlags', () => { pluginId: 'plugin-three', }); - expect(flags.toObject()).toMatchObject([ + expect(flags).toEqual([ { name: 'registered-flag-1', pluginId: 'plugin-one' }, { name: 'registered-flag-2', pluginId: 'plugin-one' }, { name: 'registered-flag-3', pluginId: 'plugin-two' }, diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx index 22e6a89329..0f794d8c44 100644 --- a/packages/core-api/src/app/FeatureFlags.tsx +++ b/packages/core-api/src/app/FeatureFlags.tsx @@ -126,45 +126,6 @@ export class UserFlags extends Map { } } -/** - * The FeatureFlagsRegistry class. - * - * This acts as a holding data structure for feature flags - * that plugins wish to register for use in Backstage. - */ - -export class FeatureFlagsRegistry extends Array { - static from(entries: FeatureFlag[]) { - Array.from(entries).forEach(entry => validateFlagName(entry.name)); - return new FeatureFlagsRegistry(...entries); - } - - push(...entries: FeatureFlag[]): number { - Array.from(entries).forEach(entry => validateFlagName(entry.name)); - return super.push(...entries); - } - - concat( - ...entries: (FeatureFlag | ConcatArray)[] - ): FeatureFlag[] { - const _concat = super.concat(...entries); - Array.from(_concat).forEach(entry => validateFlagName(entry.name)); - return _concat; - } - - toObject() { - return [...this.values()]; - } - - toJSON() { - return JSON.stringify(this.toObject()); - } - - toString() { - return this.toJSON(); - } -} - /** * Create the FeatureFlags implementation based on the API. */ @@ -177,12 +138,12 @@ export class FeatureFlags implements FeatureFlagsApi { this.registeredFeatureFlags.push(flag); } + getRegisteredFlags(): FeatureFlag[] { + return this.registeredFeatureFlags.slice(); + } + getFlags(): UserFlags { if (!this.userFlags) this.userFlags = UserFlags.load(); return this.userFlags; } - - getRegisteredFlags(): FeatureFlagsRegistry { - return FeatureFlagsRegistry.from(this.registeredFeatureFlags); - } } From 200792e434833dd5ba0d3e61b628f53b9a3a41a8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 22:04:17 +0100 Subject: [PATCH 4/8] core-api: refactor feature flags user flags access --- .../src/apis/definitions/FeatureFlagsApi.ts | 41 +++-- packages/core-api/src/app/App.tsx | 4 +- .../core-api/src/app/FeatureFlags.test.tsx | 143 +++++++----------- packages/core-api/src/app/FeatureFlags.tsx | 140 ++++++----------- packages/core-api/src/app/index.ts | 1 - packages/core-api/src/plugin/Plugin.tsx | 3 - .../CostInsightsPage/CostInsightsPage.tsx | 6 +- .../components/FeatureFlags/FeatureFlags.tsx | 26 ++-- 8 files changed, 156 insertions(+), 208 deletions(-) diff --git a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts index 76a1e4f9ba..243af562b1 100644 --- a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts @@ -15,7 +15,6 @@ */ import { ApiRef, createApiRef } from '../system'; -import { UserFlags } from '../../app/FeatureFlags'; /** * The feature flags API is used to toggle functionality to users across plugins and Backstage. @@ -29,16 +28,35 @@ import { UserFlags } from '../../app/FeatureFlags'; * to enable and disable feature flags, this API acts as another way to enable/disable. */ -export enum FeatureFlagState { - Off = 0, - On = 1, -} - -export interface FeatureFlag { +export type FeatureFlag = { name: string; pluginId: string; +}; + +export enum FeatureFlagState { + None = 0, + Active = 1, } +/** + * Options to use when saving feature flags. + */ +export type FeatureFlagsSaveOptions = { + /** + * The new feature flag states to save. + */ + states: Record; + + /** + * Whether the saves states should be merged into the existing ones, or replace them. + * + * Defaults to false. + */ + merge?: boolean; +}; + +export type UserFlags = {}; + export interface FeatureFlagsApi { /** * Registers a new feature flag. Once a feature flag has been registered it @@ -52,9 +70,14 @@ export interface FeatureFlagsApi { getRegisteredFlags(): FeatureFlag[]; /** - * Get a list of all feature flags from the current user. + * Whether the feature flag with the given name is currently activated for the user. */ - getFlags(): UserFlags; + isActive(name: string): boolean; + + /** + * Save the user's choice of feature flag states. + */ + save(options: FeatureFlagsSaveOptions): void; } export const featureFlagsApiRef: ApiRef = createApiRef({ diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 45496b9b8a..91b55a7d2e 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -54,7 +54,7 @@ import { import { useAsync } from 'react-use'; import { AppIdentity } from './AppIdentity'; import { ApiResolver, ApiFactoryRegistry } from '../apis/system'; -import { FeatureFlags } from './FeatureFlags'; +import { LocalStorageFeatureFlags } from './FeatureFlags'; type FullAppOptions = { apis: Iterable; @@ -320,7 +320,7 @@ export class PrivateAppImpl implements BackstageApp { registry.register('default', { api: featureFlagsApiRef, deps: {}, - factory: () => new FeatureFlags(), + factory: () => new LocalStorageFeatureFlags(), }); for (const factory of this.defaultApis) { registry.register('default', factory); diff --git a/packages/core-api/src/app/FeatureFlags.test.tsx b/packages/core-api/src/app/FeatureFlags.test.tsx index c04e60b069..02641d6d18 100644 --- a/packages/core-api/src/app/FeatureFlags.test.tsx +++ b/packages/core-api/src/app/FeatureFlags.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { FeatureFlags as FeatureFlagsImpl } from './FeatureFlags'; +import { LocalStorageFeatureFlags } from './FeatureFlags'; import { FeatureFlagState, FeatureFlagsApi } from '../apis/definitions'; describe('FeatureFlags', () => { @@ -22,62 +22,44 @@ describe('FeatureFlags', () => { window.localStorage.clear(); }); - describe('#getFlags', () => { + describe('getFlags', () => { let featureFlags: FeatureFlagsApi; beforeEach(() => { - featureFlags = new FeatureFlagsImpl(); + featureFlags = new LocalStorageFeatureFlags(); }); it('returns no flags', () => { - expect(featureFlags.getFlags().toObject()).toMatchObject({}); + expect(featureFlags.getRegisteredFlags()).toEqual([]); }); - it('returns the correct flags', () => { + it('loads flags from local storage', () => { window.localStorage.setItem( 'featureFlags', JSON.stringify({ 'feature-flag-one': 1, 'feature-flag-two': 1, 'feature-flag-three': 0, + 'feature-flag-four': 2, + 'feature-flag-five': 'not-valid', }), ); - featureFlags = new FeatureFlagsImpl(); - expect(featureFlags.getFlags().toObject()).toMatchObject({ - 'feature-flag-one': FeatureFlagState.On, - 'feature-flag-two': FeatureFlagState.On, - 'feature-flag-three': FeatureFlagState.Off, - }); - }); - - it('gets the correct values', () => { - window.localStorage.setItem( - 'featureFlags', - JSON.stringify({ - 'feature-flag-one': 1, - 'feature-flag-two': 0, - }), - ); - - featureFlags = new FeatureFlagsImpl(); - - expect(featureFlags.getFlags().get('feature-flag-one')).toEqual( - FeatureFlagState.On, - ); - expect(featureFlags.getFlags().get('feature-flag-two')).toEqual( - FeatureFlagState.Off, - ); - expect(featureFlags.getFlags().get('feature-flag-three')).toEqual( - FeatureFlagState.Off, - ); + expect(featureFlags.isActive('feature-flag-one')).toBe(true); + expect(featureFlags.isActive('feature-flag-two')).toBe(true); + expect(featureFlags.isActive('feature-flag-three')).toBe(false); + expect(featureFlags.isActive('feature-flag-four')).toBe(false); + expect(featureFlags.isActive('feature-flag-five')).toBe(false); }); it('sets the correct values', () => { - const flags = featureFlags.getFlags(); - flags.set('feature-flag-zero', FeatureFlagState.On); + featureFlags.save({ + states: { + 'feature-flag-zero': FeatureFlagState.Active, + }, + }); - expect(flags.get('feature-flag-zero')).toEqual(FeatureFlagState.On); + expect(featureFlags.isActive('feature-flag-zero')).toBe(true); expect(window.localStorage.getItem('featureFlags')).toEqual( '{"feature-flag-zero":1}', ); @@ -89,16 +71,20 @@ describe('FeatureFlags', () => { JSON.stringify({ 'feature-flag-one': 1, 'feature-flag-two': 0, + 'feature-flag-tree': 1, + 'feature-flag-four': 0, }), ); - featureFlags = new FeatureFlagsImpl(); - const flags = featureFlags.getFlags(); - flags.delete('feature-flag-one'); + featureFlags.save({ + states: { + 'feature-flag-one': FeatureFlagState.None, + 'feature-flag-two': FeatureFlagState.Active, + }, + }); - expect(flags.get('feature-flag-one')).toEqual(FeatureFlagState.Off); expect(window.localStorage.getItem('featureFlags')).toEqual( - '{"feature-flag-two":0}', + '{"feature-flag-two":1}', ); }); @@ -112,19 +98,25 @@ describe('FeatureFlags', () => { }), ); - const flags = featureFlags.getFlags(); - flags.clear(); + expect(featureFlags.isActive('feature-flag-one')).toBe(true); + expect(featureFlags.isActive('feature-flag-two')).toBe(true); + expect(featureFlags.isActive('feature-flag-three')).toBe(false); + + featureFlags.save({ states: {} }); + + expect(featureFlags.isActive('feature-flag-one')).toBe(false); + expect(featureFlags.isActive('feature-flag-two')).toBe(false); + expect(featureFlags.isActive('feature-flag-three')).toBe(false); - expect(flags.toObject()).toEqual({}); expect(window.localStorage.getItem('featureFlags')).toEqual('{}'); }); }); - describe('#getRegisteredFlags', () => { + describe('getRegisteredFlags', () => { let featureFlags: FeatureFlagsApi; beforeEach(() => { - featureFlags = new FeatureFlagsImpl(); + featureFlags = new LocalStorageFeatureFlags(); featureFlags.registerFlag({ name: 'registered-flag-1', pluginId: 'plugin-one', @@ -140,7 +132,7 @@ describe('FeatureFlags', () => { }); it('should return an empty list', () => { - featureFlags = new FeatureFlagsImpl(); + featureFlags = new LocalStorageFeatureFlags(); expect(featureFlags.getRegisteredFlags()).toEqual([]); }); @@ -152,6 +144,25 @@ describe('FeatureFlags', () => { ]); }); + it('should provide a copy of the list of flags', () => { + const flags = featureFlags.getRegisteredFlags(); + expect(flags).toEqual([ + { name: 'registered-flag-1', pluginId: 'plugin-one' }, + { name: 'registered-flag-2', pluginId: 'plugin-one' }, + { name: 'registered-flag-3', pluginId: 'plugin-two' }, + ]); + flags.splice(2, 1); + expect(flags).toEqual([ + { name: 'registered-flag-1', pluginId: 'plugin-one' }, + { name: 'registered-flag-2', pluginId: 'plugin-one' }, + ]); + expect(featureFlags.getRegisteredFlags()).toEqual([ + { name: 'registered-flag-1', pluginId: 'plugin-one' }, + { name: 'registered-flag-2', pluginId: 'plugin-one' }, + { name: 'registered-flag-3', pluginId: 'plugin-two' }, + ]); + }); + it('should get the correct values', () => { const getByName = (name: string) => featureFlags.getRegisteredFlags().find(flag => flag.name === name); @@ -171,44 +182,6 @@ describe('FeatureFlags', () => { }); }); - it('should append the correct value', () => { - const flags = featureFlags.getRegisteredFlags(); - - flags.push({ - name: 'registered-flag-4', - pluginId: 'plugin-three', - }); - - expect(flags).toEqual([ - { name: 'registered-flag-1', pluginId: 'plugin-one' }, - { name: 'registered-flag-2', pluginId: 'plugin-one' }, - { name: 'registered-flag-3', pluginId: 'plugin-two' }, - { name: 'registered-flag-4', pluginId: 'plugin-three' }, - ]); - }); - - it('should concat the correct values', () => { - const flags = featureFlags.getRegisteredFlags(); - const concatValues = flags.concat([ - { - name: 'registered-flag-4', - pluginId: 'plugin-three', - }, - { - name: 'registered-flag-5', - pluginId: 'plugin-four', - }, - ]); - - expect(concatValues).toMatchObject([ - { name: 'registered-flag-1', pluginId: 'plugin-one' }, - { name: 'registered-flag-2', pluginId: 'plugin-one' }, - { name: 'registered-flag-3', pluginId: 'plugin-two' }, - { name: 'registered-flag-4', pluginId: 'plugin-three' }, - { name: 'registered-flag-5', pluginId: 'plugin-four' }, - ]); - }); - it('throws an error if length is less than three characters', () => { expect(() => featureFlags.registerFlag({ diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx index 0f794d8c44..73215481b7 100644 --- a/packages/core-api/src/app/FeatureFlags.tsx +++ b/packages/core-api/src/app/FeatureFlags.tsx @@ -18,19 +18,9 @@ import { FeatureFlagState, FeatureFlagsApi, FeatureFlag, + FeatureFlagsSaveOptions, } from '../apis/definitions'; -/** - * Helper method for validating compatibility and flag name. - */ -export function validateBrowserCompat(): void { - if (!('localStorage' in window)) { - throw new Error( - 'Feature Flags are not supported on browsers without the Local Storage API', - ); - } -} - export function validateFlagName(name: string): void { if (name.length < 3) { throw new Error( @@ -52,86 +42,12 @@ export function validateFlagName(name: string): void { } } -/** - * The UserFlags class. - * - * This acts as a data structure for the user's feature flags. You - * can use this to retrieve, add, edit, delete, clear and save the user's - * feature flags to the local browser for persisted storage. - */ -export class UserFlags extends Map { - static load(): UserFlags { - validateBrowserCompat(); - - try { - const jsonString = window.localStorage.getItem('featureFlags') as string; - const json = JSON.parse(jsonString); - return new this(Object.entries(json)); - } catch (err) { - return new this([]); - } - } - - get(name: string): FeatureFlagState { - return super.get(name) || FeatureFlagState.Off; - } - - set(name: string, state: FeatureFlagState): this { - validateFlagName(name); - const output = super.set(name, state); - this.save(); - return output; - } - - toggle(name: string): FeatureFlagState { - if (super.get(name) === FeatureFlagState.On) { - super.set(name, FeatureFlagState.Off); - } else { - super.set(name, FeatureFlagState.On); - } - return super.get(name) || FeatureFlagState.Off; - } - - delete(name: string): boolean { - const output = super.delete(name); - this.save(); - return output; - } - - clear(): void { - super.clear(); - this.save(); - } - - save(): void { - window.localStorage.setItem( - 'featureFlags', - JSON.stringify(this.toObject()), - ); - } - - toObject() { - return Array.from(this.entries()).reduce( - (obj, [key, value]) => ({ ...obj, [key]: value }), - {}, - ); - } - - toJSON() { - return JSON.stringify(this.toObject()); - } - - toString() { - return this.toJSON(); - } -} - /** * Create the FeatureFlags implementation based on the API. */ -export class FeatureFlags implements FeatureFlagsApi { +export class LocalStorageFeatureFlags implements FeatureFlagsApi { private registeredFeatureFlags: FeatureFlag[] = []; - private userFlags: UserFlags | undefined; + private flags?: Map; registerFlag(flag: FeatureFlag) { validateFlagName(flag.name); @@ -142,8 +58,52 @@ export class FeatureFlags implements FeatureFlagsApi { return this.registeredFeatureFlags.slice(); } - getFlags(): UserFlags { - if (!this.userFlags) this.userFlags = UserFlags.load(); - return this.userFlags; + isActive(name: string): boolean { + if (!this.flags) { + this.flags = this.load(); + } + return this.flags.get(name) === FeatureFlagState.Active; + } + + save(options: FeatureFlagsSaveOptions): void { + if (!this.flags) { + this.flags = this.load(); + } + if (!options.merge) { + this.flags.clear(); + } + for (const [name, state] of Object.entries(options.states)) { + this.flags.set(name, state); + } + + const enabled = Array.from(this.flags.entries()).filter( + ([, state]) => state === FeatureFlagState.Active, + ); + window.localStorage.setItem( + 'featureFlags', + JSON.stringify(Object.fromEntries(enabled)), + ); + } + + private load(): Map { + try { + const jsonStr = window.localStorage.getItem('featureFlags'); + if (!jsonStr) { + return new Map(); + } + const json = JSON.parse(jsonStr) as unknown; + if (typeof json !== 'object' || json === null || Array.isArray(json)) { + return new Map(); + } + + const entries = Object.entries(json).filter(([name, value]) => { + validateFlagName(name); + return value === FeatureFlagState.Active; + }); + + return new Map(entries); + } catch { + return new Map(); + } } } diff --git a/packages/core-api/src/app/index.ts b/packages/core-api/src/app/index.ts index 003b71e353..17610ea3ee 100644 --- a/packages/core-api/src/app/index.ts +++ b/packages/core-api/src/app/index.ts @@ -14,6 +14,5 @@ * limitations under the License. */ -export { FeatureFlags } from './FeatureFlags'; export { useApp } from './AppContext'; export * from './types'; diff --git a/packages/core-api/src/plugin/Plugin.tsx b/packages/core-api/src/plugin/Plugin.tsx index 866d430070..d69bb7e721 100644 --- a/packages/core-api/src/plugin/Plugin.tsx +++ b/packages/core-api/src/plugin/Plugin.tsx @@ -15,7 +15,6 @@ */ import { PluginConfig, PluginOutput, BackstagePlugin } from './types'; -import { validateBrowserCompat, validateFlagName } from '../app/FeatureFlags'; import { AnyApiFactory } from '../apis'; export class PluginImpl { @@ -57,8 +56,6 @@ export class PluginImpl { }, featureFlags: { register(name) { - validateBrowserCompat(); - validateFlagName(name); outputs.push({ type: 'feature-flag', name }); }, }, diff --git a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx index 638f6fef00..52f70d2756 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx @@ -55,9 +55,7 @@ import { useSubtleTypographyStyles } from '../../utils/styles'; export const CostInsightsPage = () => { const classes = useSubtleTypographyStyles(); - const flags = useApi(featureFlagsApiRef).getFlags(); - // There is not currently a UI to set feature flags - // flags.set('cost-insights-currencies', FeatureFlagState.On); + const featureFlags = useApi(featureFlagsApiRef); const client = useApi(costInsightsApiRef); const config = useConfig(); const groups = useGroups(); @@ -211,7 +209,7 @@ export const CostInsightsPage = () => { - {!!flags.get('cost-insights-currencies') && ( + {featureFlags.isActive('cost-insights-currencies') && ( { const featureFlagsApi = useApi(featureFlagsApiRef); const featureFlags = featureFlagsApi.getRegisteredFlags(); - const initialFlagState = featureFlags.reduce( - (result, featureFlag: FeatureFlag) => { - const state = featureFlagsApi.getFlags().get(featureFlag.name); - result[featureFlag.name] = state; - return result; - }, - {} as Record, + const initialFlagState = Object.fromEntries( + featureFlags.map(({ name }) => [name, featureFlagsApi.isActive(name)]), ); - const [state, setState] = useState>( - initialFlagState, - ); + const [state, setState] = useState>(initialFlagState); const toggleFlag = useCallback( (flagName: string) => { - const newState = featureFlagsApi.getFlags().toggle(flagName); + const newState = featureFlagsApi.isActive(flagName) + ? FeatureFlagState.None + : FeatureFlagState.Active; + + featureFlagsApi.save({ + states: { [flagName]: newState }, + merge: true, + }); setState(prevState => ({ ...prevState, - [flagName]: newState, + [flagName]: newState === FeatureFlagState.Active, })); - featureFlagsApi.getFlags().save(); }, [featureFlagsApi], ); From 471b15aa18161651470f252760f1b8863dcfbac6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 22:10:23 +0100 Subject: [PATCH 5/8] docs: update feature flag docs --- docs/reference/createPlugin-feature-flags.md | 46 +++++--------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/docs/reference/createPlugin-feature-flags.md b/docs/reference/createPlugin-feature-flags.md index 097294e55f..7108941edd 100644 --- a/docs/reference/createPlugin-feature-flags.md +++ b/docs/reference/createPlugin-feature-flags.md @@ -8,12 +8,6 @@ The `featureFlags` object passed to the `register` function makes it possible for plugins to register Feature Flags in Backstage for users to opt into. You can use this to split out logic in your code for manual A/B testing, etc. -```typescript -export type FeatureFlagsHooks = { - register(name: string): void; -}; -``` - Here's a code sample: ```typescript @@ -21,8 +15,7 @@ import { createPlugin } from '@backstage/core'; export default createPlugin({ id: 'welcome', - register({ router, featureFlags }) { - // router.registerRoute('/', Component); + register({ featureFlags }) { featureFlags.register('enable-example-feature'); }, }); @@ -30,41 +23,22 @@ export default createPlugin({ ## Using with useApi -To use it, you'll first need to register the `FeatureFlags` API via -`ApiRegistry` in your `apis.ts` in your App: - -```tsx -import { - ApiHolder, - ApiRegistry, - featureFlagsApiRef, - FeatureFlags, -} from '@backstage/core'; - -const builder = ApiRegistry.builder(); -builder.add(featureFlagsApiRef, new FeatureFlags()); - -export default builder.build() as ApiHolder; -``` - -Then, later, you can directly use it via `useApi`: +To inspect the state of a feature flag inside your plugin, you can use the +`FeatureFlagsApi`, accessed via the `FeatureFlagsApiRef`. For example: ```tsx import React, { FC } from 'react'; import { Button } from '@material-ui/core'; -import { FeatureFlagState, featureFlagsApiRef, useApi } from '@backstage/core'; +import { featureFlagsApiRef, useApi } from '@backstage/core'; -const ExampleButton: FC<{}> = () => { - const flags = useApi(featureFlagsApiRef).getFlags(); - - const handleClick = () => { - flags.set('enable-example-feature', FeatureFlagState.On); - }; +const ExamplePage: FC<{}> = () => { + const featureFlags = useApi(featureFlagsApiRef); return ( - +
+ + { featureFlags.isActive('enable-example-feature') && } +
); }; ``` From 1f010f7828f47d37a523bb06ba8e2c16f5ef7d3a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 22:14:23 +0100 Subject: [PATCH 6/8] core-api: move LocalStorageFeatureFlags to api implementations --- .../LocalStorageFeatureFlags.test.tsx} | 4 ++-- .../LocalStorageFeatureFlags.tsx} | 2 +- .../implementations/FeatureFlagsApi/index.ts | 17 +++++++++++++++++ .../core-api/src/apis/implementations/index.ts | 3 ++- packages/core-api/src/app/App.tsx | 2 +- 5 files changed, 23 insertions(+), 5 deletions(-) rename packages/core-api/src/{app/FeatureFlags.test.tsx => apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx} (98%) rename packages/core-api/src/{app/FeatureFlags.tsx => apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx} (99%) create mode 100644 packages/core-api/src/apis/implementations/FeatureFlagsApi/index.ts diff --git a/packages/core-api/src/app/FeatureFlags.test.tsx b/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx similarity index 98% rename from packages/core-api/src/app/FeatureFlags.test.tsx rename to packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx index 02641d6d18..2902e399c9 100644 --- a/packages/core-api/src/app/FeatureFlags.test.tsx +++ b/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import { LocalStorageFeatureFlags } from './FeatureFlags'; -import { FeatureFlagState, FeatureFlagsApi } from '../apis/definitions'; +import { LocalStorageFeatureFlags } from './LocalStorageFeatureFlags'; +import { FeatureFlagState, FeatureFlagsApi } from '../../definitions'; describe('FeatureFlags', () => { beforeEach(() => { diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx similarity index 99% rename from packages/core-api/src/app/FeatureFlags.tsx rename to packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx index 73215481b7..00b3eff906 100644 --- a/packages/core-api/src/app/FeatureFlags.tsx +++ b/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx @@ -19,7 +19,7 @@ import { FeatureFlagsApi, FeatureFlag, FeatureFlagsSaveOptions, -} from '../apis/definitions'; +} from '../../definitions'; export function validateFlagName(name: string): void { if (name.length < 3) { diff --git a/packages/core-api/src/apis/implementations/FeatureFlagsApi/index.ts b/packages/core-api/src/apis/implementations/FeatureFlagsApi/index.ts new file mode 100644 index 0000000000..33990584f3 --- /dev/null +++ b/packages/core-api/src/apis/implementations/FeatureFlagsApi/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { LocalStorageFeatureFlags } from './LocalStorageFeatureFlags'; diff --git a/packages/core-api/src/apis/implementations/index.ts b/packages/core-api/src/apis/implementations/index.ts index 30aeb81d44..d0df2760ab 100644 --- a/packages/core-api/src/apis/implementations/index.ts +++ b/packages/core-api/src/apis/implementations/index.ts @@ -23,7 +23,8 @@ export * from './auth'; export * from './AlertApi'; export * from './AppThemeApi'; export * from './ConfigApi'; -export * from './ErrorApi'; export * from './DiscoveryApi'; +export * from './ErrorApi'; +export * from './FeatureFlagsApi'; export * from './OAuthRequestApi'; export * from './StorageApi'; diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 91b55a7d2e..9c38c20b44 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -50,11 +50,11 @@ import { useApi, AnyApiFactory, ApiHolder, + LocalStorageFeatureFlags, } from '../apis'; import { useAsync } from 'react-use'; import { AppIdentity } from './AppIdentity'; import { ApiResolver, ApiFactoryRegistry } from '../apis/system'; -import { LocalStorageFeatureFlags } from './FeatureFlags'; type FullAppOptions = { apis: Iterable; From cbab5bbf8aefc2df4a737204d7f2d1a3544eadea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 22:46:21 +0100 Subject: [PATCH 7/8] changesets: added changeset for FeatureFlagsApi refactoring --- .changeset/swift-emus-mate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/swift-emus-mate.md diff --git a/.changeset/swift-emus-mate.md b/.changeset/swift-emus-mate.md new file mode 100644 index 0000000000..1903742115 --- /dev/null +++ b/.changeset/swift-emus-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-api': minor +--- + +Refactored the FeatureFlagsApi to make it easier to re-implement. Existing usage of particularly getUserFlags can be replaced with isActive() or save(). From b1b3f094f10470fff5feee99a89a365e729a2284 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 29 Oct 2020 15:51:54 +0100 Subject: [PATCH 8/8] docs: tweak feature-flags docs --- docs/reference/createPlugin-feature-flags.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/createPlugin-feature-flags.md b/docs/reference/createPlugin-feature-flags.md index 7108941edd..bcea80e26b 100644 --- a/docs/reference/createPlugin-feature-flags.md +++ b/docs/reference/createPlugin-feature-flags.md @@ -24,14 +24,14 @@ export default createPlugin({ ## Using with useApi To inspect the state of a feature flag inside your plugin, you can use the -`FeatureFlagsApi`, accessed via the `FeatureFlagsApiRef`. For example: +`FeatureFlagsApi`, accessed via the `featureFlagsApiRef`. For example: ```tsx import React, { FC } from 'react'; import { Button } from '@material-ui/core'; import { featureFlagsApiRef, useApi } from '@backstage/core'; -const ExamplePage: FC<{}> = () => { +const ExamplePage = () => { const featureFlags = useApi(featureFlagsApiRef); return (