Merge pull request #3149 from spotify/rugvip/ff

core-api: refactor and slim down the FeatureFlagsApi
This commit is contained in:
Patrik Oldsberg
2020-10-29 16:58:52 +01:00
committed by GitHub
16 changed files with 290 additions and 383 deletions
+5
View File
@@ -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().
+10 -36
View File
@@ -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: FeatureFlagName): 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 = () => {
const featureFlags = useApi(featureFlagsApiRef);
return (
<Button variant="contained" color="primary" onClick={handleClick}>
Enable the 'enable-example-feature' feature flag
</Button>
<div>
<MyPluginWidget>
{ featureFlags.isActive('enable-example-feature') && <ExperimentalPluginWidget> }
</div>
);
};
```
@@ -15,8 +15,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.
@@ -30,31 +28,56 @@ import { FeatureFlagName } from '../../plugin';
* to enable and disable feature flags, this API acts as another way to enable/disable.
*/
export type FeatureFlag = {
name: string;
pluginId: string;
};
export enum FeatureFlagState {
Off = 0,
On = 1,
None = 0,
Active = 1,
}
/**
* Options to use when saving feature flags.
*/
export type FeatureFlagsSaveOptions = {
/**
* The new feature flag states to save.
*/
states: Record<string, FeatureFlagState>;
/**
* 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 {
/**
* 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[];
/**
* Get a list of all feature flags from the current user.
*/
getFlags(): UserFlags;
registerFlag(flag: FeatureFlag): void;
/**
* Get a list of all registered flags.
*/
getRegisteredFlags(): FeatureFlagsRegistry;
}
getRegisteredFlags(): FeatureFlag[];
export interface FeatureFlagsRegistryItem {
pluginId: string;
name: FeatureFlagName;
/**
* Whether the feature flag with the given name is currently activated for the user.
*/
isActive(name: string): boolean;
/**
* Save the user's choice of feature flag states.
*/
save(options: FeatureFlagsSaveOptions): void;
}
export const featureFlagsApiRef: ApiRef<FeatureFlagsApi> = createApiRef({
@@ -14,70 +14,52 @@
* limitations under the License.
*/
import { FeatureFlags as FeatureFlagsImpl } from './FeatureFlags';
import { FeatureFlagState, FeatureFlagsApi } from '../apis/definitions';
import { LocalStorageFeatureFlags } from './LocalStorageFeatureFlags';
import { FeatureFlagState, FeatureFlagsApi } from '../../definitions';
describe('FeatureFlags', () => {
beforeEach(() => {
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,33 +98,65 @@ 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.registeredFeatureFlags = [
{ name: 'registered-flag-1', pluginId: 'plugin-one' },
{ name: 'registered-flag-2', pluginId: 'plugin-one' },
{ name: 'registered-flag-3', pluginId: 'plugin-two' },
];
featureFlags = new LocalStorageFeatureFlags();
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 = [];
expect(featureFlags.getRegisteredFlags().toObject()).toEqual([]);
featureFlags = new LocalStorageFeatureFlags();
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' },
]);
});
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' },
@@ -164,48 +182,9 @@ describe('FeatureFlags', () => {
});
});
it('should append the correct value', () => {
const flags = featureFlags.getRegisteredFlags();
flags.push({
name: 'registered-flag-4',
pluginId: 'plugin-three',
});
expect(flags.toObject()).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' },
]);
});
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', () => {
const flags = featureFlags.getRegisteredFlags();
expect(() =>
flags.push({
featureFlags.registerFlag({
name: 'ab',
pluginId: 'plugin-three',
}),
@@ -213,9 +192,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 +202,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 +211,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',
}),
@@ -0,0 +1,109 @@
/*
* 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.
*/
import {
FeatureFlagState,
FeatureFlagsApi,
FeatureFlag,
FeatureFlagsSaveOptions,
} from '../../definitions';
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.`,
);
}
if (name.length > 150) {
throw new Error(
`The '${name}' feature flag must not exceed 150 characters.`,
);
}
if (!name.match(/^[a-z]+[a-z0-9-]+$/)) {
throw new Error(
`The '${name}' feature flag must start with a lowercase letter and only contain lowercase letters, numbers and hyphens. ` +
'Examples: feature-flag-one, alpha, release-2020',
);
}
}
/**
* Create the FeatureFlags implementation based on the API.
*/
export class LocalStorageFeatureFlags implements FeatureFlagsApi {
private registeredFeatureFlags: FeatureFlag[] = [];
private flags?: Map<string, FeatureFlagState>;
registerFlag(flag: FeatureFlag) {
validateFlagName(flag.name);
this.registeredFeatureFlags.push(flag);
}
getRegisteredFlags(): FeatureFlag[] {
return this.registeredFeatureFlags.slice();
}
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<string, FeatureFlagState> {
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();
}
}
}
@@ -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';
@@ -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';
+12 -9
View File
@@ -35,7 +35,6 @@ import {
AppThemeApi,
ConfigApi,
identityApiRef,
FeatureFlagsRegistryItem,
} from '../apis/definitions';
import { AppThemeProvider } from './AppThemeProvider';
@@ -51,6 +50,7 @@ import {
useApi,
AnyApiFactory,
ApiHolder,
LocalStorageFeatureFlags,
} from '../apis';
import { useAsync } from 'react-use';
import { AppIdentity } from './AppIdentity';
@@ -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 LocalStorageFeatureFlags(),
});
for (const factory of this.defaultApis) {
registry.register('default', factory);
}
-187
View File
@@ -1,187 +0,0 @@
/*
* 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.
*/
import { FeatureFlagName } from '../plugin/types';
import {
FeatureFlagState,
FeatureFlagsApi,
FeatureFlagsRegistryItem,
} 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: FeatureFlagName): void {
if (name.length < 3) {
throw new Error(
`The '${name}' feature flag must have a minimum length of three characters.`,
);
}
if (name.length > 150) {
throw new Error(
`The '${name}' feature flag must not exceed 150 characters.`,
);
}
if (!name.match(/^[a-z]+[a-z0-9-]+$/)) {
throw new Error(
`The '${name}' feature flag must start with a lowercase letter and only contain lowercase letters, numbers and hyphens. ` +
'Examples: feature-flag-one, alpha, release-2020',
);
}
}
/**
* 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<FeatureFlagName, FeatureFlagState> {
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: FeatureFlagName): FeatureFlagState {
return super.get(name) || FeatureFlagState.Off;
}
set(name: FeatureFlagName, state: FeatureFlagState): this {
validateFlagName(name);
const output = super.set(name, state);
this.save();
return output;
}
toggle(name: FeatureFlagName): 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: FeatureFlagName): 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();
}
}
/**
* 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<FeatureFlagsRegistryItem> {
static from(entries: FeatureFlagsRegistryItem[]) {
Array.from(entries).forEach(entry => validateFlagName(entry.name));
return new FeatureFlagsRegistry(...entries);
}
push(...entries: FeatureFlagsRegistryItem[]): number {
Array.from(entries).forEach(entry => validateFlagName(entry.name));
return super.push(...entries);
}
concat(
...entries: (
| FeatureFlagsRegistryItem
| ConcatArray<FeatureFlagsRegistryItem>
)[]
): FeatureFlagsRegistryItem[] {
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.
*/
export class FeatureFlags implements FeatureFlagsApi {
public registeredFeatureFlags: FeatureFlagsRegistryItem[] = [];
private userFlags: UserFlags | undefined;
getFlags(): UserFlags {
if (!this.userFlags) this.userFlags = UserFlags.load();
return this.userFlags;
}
getRegisteredFlags(): FeatureFlagsRegistry {
return FeatureFlagsRegistry.from(this.registeredFeatureFlags);
}
}
-1
View File
@@ -14,6 +14,5 @@
* limitations under the License.
*/
export { FeatureFlags } from './FeatureFlags';
export { useApp } from './AppContext';
export * from './types';
-3
View File
@@ -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 });
},
},
+2 -4
View File
@@ -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;
};
@@ -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,
@@ -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 = () => {
</Typography>
</Box>
<Box minHeight={40} maxHeight={60} display="flex">
{!!flags.get('cost-insights-currencies') && (
{featureFlags.isActive('cost-insights-currencies') && (
<Box mr={1}>
<CurrencySelect
currency={currency}
@@ -16,9 +16,7 @@
import React, { useCallback, useState } from 'react';
import {
FeatureFlagName,
featureFlagsApiRef,
FeatureFlagsRegistryItem,
FeatureFlagState,
InfoCard,
useApi,
@@ -30,29 +28,28 @@ import { FlagItem } from './FeatureFlagsItem';
export const FeatureFlags = () => {
const featureFlagsApi = useApi(featureFlagsApiRef);
const featureFlags = featureFlagsApi.getRegisteredFlags();
const initialFlagState = featureFlags.reduce(
(result, featureFlag: FeatureFlagsRegistryItem) => {
const state = featureFlagsApi.getFlags().get(featureFlag.name);
result[featureFlag.name] = state;
return result;
},
{} as Record<FeatureFlagName, FeatureFlagState>,
const initialFlagState = Object.fromEntries(
featureFlags.map(({ name }) => [name, featureFlagsApi.isActive(name)]),
);
const [state, setState] = useState<Record<FeatureFlagName, FeatureFlagState>>(
initialFlagState,
);
const [state, setState] = useState<Record<string, boolean>>(initialFlagState);
const toggleFlag = useCallback(
(flagName: FeatureFlagName) => {
const newState = featureFlagsApi.getFlags().toggle(flagName);
(flagName: string) => {
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],
);
@@ -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;
};