Skip to content

Commit

Permalink
chore: feature flags support for parallel development (flyteorg#267)
Browse files Browse the repository at this point in the history
* chore: feature flags support for paralel development
* Update Contributing.md with FeatureFlags usage
* Update README.md to include feature flags

Signed-off-by: Nastya Rusina <[email protected]>
  • Loading branch information
anrusina authored Jan 19, 2022
1 parent 076c744 commit fb99a8f
Show file tree
Hide file tree
Showing 11 changed files with 382 additions and 60 deletions.
104 changes: 92 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,7 @@
First off, thank you for thinking about contributing!
Below you’ll find instructions that will hopefully guide you through how to contribute to, fix, and improve Flyte Console.

## Storybook

This project has support for [Storybook](https://storybook.js.org/).
Component stories live next to the components they test, in a `__stories__`
directory, with the filename pattern `{Component}.stories.tsx`.

You can run storybook with `yarn run storybook`, and view the stories at http://localhost:9001.

## Protobuf and the Network Tab
## Protobuf and Debug Output

Communication with the Flyte Admin API is done using Protobuf as the
request/response format. Protobuf is a binary format, which means looking at
Expand All @@ -20,10 +12,8 @@ each network request is logged to the console with it's URL followed by the
decoded Protobuf payload. You must have debug output enabled (on by default in
development) to see these messages.

## Debug Output

This application makes use of the [debug](https://github.com/visionmedia/debug)
libary to provide namespaced debug output in the browser console. In
libary to provide namespaced **debug output** in the browser console. In
development, all debug output is enabled. For other environments, the debug
output must be enabled manually. You can do this by setting a flag in
localStorage using the console: `localStorage.debug = 'flyte:*'`. Each module in
Expand All @@ -32,6 +22,96 @@ a single module, you can specify that one specifically
(ex. ``localStorage.debug = 'flyte:adminEntity'`` to only see decoded Flyte
Admin API requests).

## Storybook

This project has support for [Storybook](https://storybook.js.org/).
Component stories live next to the components they test, in a `__stories__`
directory, with the filename pattern `{Component}.stories.tsx`.

You can run storybook with `yarn run storybook`, and view the stories at http://localhost:9001.

## Feature flags

We are using our internal feature flag solution to allow continuos integration,
while features are in development.
All flags currently available could be found in [/FeatureFlags/defaultConfig.ts](./src/basics/FeatureFlags/defaultConfig.ts)
file. Most of them under active development, which means we don't guarantee it will work as you expect.

If you want to add your own flag, you need to add it to both `enum FeatureFlag` and `defaultFlagConfig`
under production section.
Initally all flags must be disabled, meaning you code path should not be executed by default.

**Example - adding flags:**

```javascript
enum FeatureFlags {
...
AddNewPage: 'add-new-page'
UseCommonPath: 'use-common-path'
}

export const defaultFlagConfig: FeatureFlagConfig = {
...
'add-new-page': false, // default/prior behavior doesn't include new page
'use-common-path': true, // default/prior behavior uses common path
};
```

To use flags in code you need to ensure that the most top level component is wrapped by `FeatureFlagsProvider`.
By default we are wrapping top component in Apps file, so if you do not plan to include
feature flags checks in the `\*.tests.tsx` - you should be good to go.
To check flag's value use `useFeatureFlag` hook.

**Example - flag usage**:

```javascript
import { FeatureFlag, useFeatureFlag } from 'basics/FeatureFlags';

export function MyComponent(props: Props): React.ReactNode {
...
const isFlagEnabled = useFeatureFlag(FeatureFlag.AddNewPage);

return isFlagEnabled ? <NewPage ...props/> : null;
}
```

During your local development you can either:
* temporarily switch flags value in runtimeConfig as:
```javascript
let runtimeConfig = {
...defaultFlagConfig,
'add-new-page': true,
};
```
* turn flag on/off from the devTools console in Chrome
![SetFeatureFlagFromConsole](https://user-images.githubusercontent.com/55718143/150002962-f12bbe57-f221-4bbd-85e3-717aa0221e89.gif)

#### Unit tests

If you plan to test non-default flag value in your unit tests, make sure to wrap your component with `FeatureFlagsProvider`.
Use `window.setFeatureFlag(flag, newValue)` function to set needed value and `window.clearRuntimeConfig()`
to return to defaults. Beware to comment out/remove any changes in `runtimeConfig` during testing;

```javascript
function TestWrapper() {
return <FeatureFlagsProvider> <TestContent /> </FeatureFlagsProvider>
}

describe('FeatureFlags', () => {
afterEach(() => {
window.clearRuntimeConfig(); // clean up flags
});

it('Test', async () => {
render(<TestWrapper />);

window.setFeatureFlag(FeatureFlag.FlagInQuestion, true);
await waitFor(() => {
// check after flag changed value
});
});
```
## Google Analytics
This application makes use of the `react-ga4 <https://github.com/PriceRunner/react-ga4>`_
Expand Down
35 changes: 27 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@

## 📦 Install Dependencies
Running flyteconsole locally requires [NodeJS](https://nodejs.org) and
[yarn](https://yarnpkg.com). Once these are installed, all of the dependencies
can be installed by running `yarn install` in the project directory.
[yarn](https://yarnpkg.com). Once these are installed, you can run application locally.
For help with installing dependencies look into
[Installation section](https://github.com/flyteorg/flyteconsole/blob/narusina/fflags/CONTRIBUTING.md#-install-dependencies).

## 🚀 Quick Start
1. Follow [Start a Local flyte backend](https://docs.flyte.org/en/latest/getting_started/first_run.html), like:
1. Follow [Start a Local flyte backend](https://docs.flyte.org/en/latest/getting_started/index.html), like:
```bash
docker run --rm --privileged -p 30081:30081 -p 30082:30082 -p 30084:30084 cr.flyte.org/flyteorg/flyte-sandbox
```
Expand Down Expand Up @@ -110,16 +111,34 @@ docker run -p 8080:8080 -e BASE_URL="/console" -e CONFIG_DIR="/etc/flyte/config"

### Run the server

To start the local development server, run `yarn start`. This will spin up a
Webpack development server, compile all of the code into bundles, and start the
NodeJS server on the default port (3000). All requests to the NodeJS server will
be stalled until the bundles have finished. The application will be accessible
To start the local development server run:
```bash
yarn install # to install node_modules
yarn start # to start application
```
This will spin up a Webpack development server, compile all of the code into bundles,
and start the NodeJS server on the default port (3000). All requests to the NodeJS server
will be stalled until the bundles have finished. The application will be accessible
at http://localhost:3000 (if using the default port).


## 🛠 Development

Please find most information about developement details in [CONTRIBUTING.md](CONTRIBUTING.md)
For continious development we are using:
* **[Protobuf and Debug Output](https://github.com/flyteorg/flyteconsole/blob/narusina/fflags/CONTRIBUTING.md#protobuf-and-debug-output)**.
Protobuf is a binary response/request format, which makes _Network Tab_ hardly useful.
To get more info on requests - use our Debug Output

* **[Storybook](https://github.com/flyteorg/flyteconsole/blob/narusina/fflags/CONTRIBUTING.md#storybook)**
\- used for component stories and base UI testing.

* **[Feature flags](https://github.com/flyteorg/flyteconsole/blob/narusina/fflags/CONTRIBUTING.md#feature-flags)**
\- allows to enable/disable specific code paths. Used to simplify continious development.

* **[Google Analytics](https://github.com/flyteorg/flyteconsole/blob/narusina/fflags/CONTRIBUTING.md#google-analytics)**
\- adds tracking code to the app or website. To disable use `ENABLE_GA=false`

More info on each section could be found at [CONTRIBUTING.md](CONTRIBUTING.md)

### CORS Proxying: Recommended setup

Expand Down
69 changes: 69 additions & 0 deletions src/basics/FeatureFlags/FeatureFlags.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as React from 'react';
import { render, screen, waitFor } from '@testing-library/react';

import { FeatureFlagsProvider, useFeatureFlag } from '.';
import { FeatureFlag } from './defaultConfig';

function TestContent() {
const enabledTestFlag = useFeatureFlag(FeatureFlag.TestFlagUndefined);
return (
<FeatureFlagsProvider>
<button>{enabledTestFlag ? 'Enabled' : 'Disabled'}</button>
</FeatureFlagsProvider>
);
}

function TestPage() {
return (
<FeatureFlagsProvider>
<TestContent />
</FeatureFlagsProvider>
);
}

declare global {
interface Window {
setFeatureFlag: (flag: FeatureFlag, newValue: boolean) => void;
getFeatureFlag: (flag: FeatureFlag) => boolean;
clearRuntimeConfig: () => void;
}
}

describe('FeatureFlags', () => {
beforeEach(() => {
render(<TestPage />);
});

afterEach(() => {
window.clearRuntimeConfig();
});

it('Feature flags can be read/set from dev tools', async () => {
// flag defined and return proper value
expect(window.getFeatureFlag(FeatureFlag.TestFlagTrue)).toBeTruthy();
// flag undefined and returns false
expect(
window.getFeatureFlag(FeatureFlag.TestFlagUndefined)
).toBeFalsy();

window.setFeatureFlag(FeatureFlag.TestFlagUndefined, true);
await waitFor(() => {
// check that flag cghanged value
expect(
window.getFeatureFlag(FeatureFlag.TestFlagUndefined)
).toBeTruthy();
});
});

it('useFeatureFlags returns proper live value', async () => {
// default value - flag is disabled
expect(screen.getByText(/Disabled/i)).toBeTruthy();

// Enable flag
window.setFeatureFlag(FeatureFlag.TestFlagUndefined, true);
await waitFor(() => {
// check that component was updated accordingly
expect(screen.getByText(/Enabled/i)).toBeTruthy();
});
});
});
23 changes: 23 additions & 0 deletions src/basics/FeatureFlags/defaultConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Default Feature Flag config - used for features in developement.
*/

export enum FeatureFlag {
// Test flag is created only for unit-tests
TestFlagUndefined = 'test-flag-undefined',
TestFlagTrue = 'test-flag-true',

// Production flags
LaunchPlan = 'launch-plan'
}

export type FeatureFlagConfig = { [k: string]: boolean };

export const defaultFlagConfig: FeatureFlagConfig = {
// Test
'test-flag-true': true,

// Production - new code should be turned off by default
// If you need to turn it on locally -> update runtimeConfig in ./index.tsx file
'launch-plan': false
};
102 changes: 102 additions & 0 deletions src/basics/FeatureFlags/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Feature Flag provider - allows a multi-stage development.
*/
import * as React from 'react';
import {
createContext,
useCallback,
useContext,
useEffect,
useState
} from 'react';
import { isDevEnv, isTestEnv } from 'common/env';
import {
defaultFlagConfig,
FeatureFlag,
FeatureFlagConfig
} from './defaultConfig';

export { FeatureFlag } from './defaultConfig';

// To turn on flag for local development only - update flag value here
// REMOVE change prior to commit
let runtimeConfig: FeatureFlagConfig = {
...defaultFlagConfig
// 'test-flag-true': true, <== locally turns flag on
};

interface FeatureFlagState {
flags: FeatureFlagConfig;
setFeatureFlag: (flag: FeatureFlag, newValue: boolean) => void;
getFeatureFlag: (flag: FeatureFlag) => boolean;
}

interface FeatureFlagProviderProps {
children?: React.ReactNode;
}

/** FeatureFlagContext - used only if ContextProvider wasn't initialized */
const FeatureFlagContext = createContext<FeatureFlagState>({
flags: defaultFlagConfig,
setFeatureFlag: () => {
/* Provider is not initialized */
},
getFeatureFlag: () => false
});

/** useFeatureFlag - should be used to get flag value */
export const useFeatureFlag = (flag: FeatureFlag) =>
useContext(FeatureFlagContext).getFeatureFlag(flag);

/** useFatureFlagContext - could be used to set flags from code */
export const useFatureFlagContext = () => useContext(FeatureFlagContext);

/** FeatureFlagsProvider - should wrap top level component for Production or feature flag related testing*/
export const FeatureFlagsProvider = (props: FeatureFlagProviderProps) => {
const [flags, setFlags] = useState<FeatureFlagConfig>({
...defaultFlagConfig,
...runtimeConfig
});

const setFeatureFlag = useCallback(
(flag: FeatureFlag, newValue: boolean) => {
runtimeConfig[flag] = newValue;
setFlags({ ...defaultFlagConfig, ...runtimeConfig });
},
[]
);

const getFeatureFlag = useCallback(
(flag: FeatureFlag) => {
if (isDevEnv() && flags[flag] === undefined) {
throw `Default config value is absent for ${flag}`;
}
return flags[flag] ?? false;
},
[flags]
);

const clearRuntimeConfig = useCallback(() => {
runtimeConfig = { ...defaultFlagConfig };
}, []);

useEffect(() => {
if (isDevEnv() || isTestEnv()) {
// Allow manual change of feature flags from devtools
window['setFeatureFlag'] = setFeatureFlag;
window['getFeatureFlag'] = getFeatureFlag;
if (isTestEnv()) {
// allow reset flags to default - should be used in testing environment only
window['clearRuntimeConfig'] = clearRuntimeConfig;
}
}
}, [setFeatureFlag, getFeatureFlag, clearRuntimeConfig]);

return (
<FeatureFlagContext.Provider
value={{ flags, setFeatureFlag, getFeatureFlag }}
>
{props.children}
</FeatureFlagContext.Provider>
);
};
3 changes: 3 additions & 0 deletions src/common/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ import { Env } from 'config/types';
/** equivalent to process.env in server and client */
// tslint:disable-next-line:no-any
export const env: Env = Object.assign({}, process.env, window.env);

export const isDevEnv = () => env.NODE_ENV === 'development';
export const isTestEnv = () => env.NODE_ENV === 'test';
Loading

0 comments on commit fb99a8f

Please sign in to comment.