diff --git a/package.json b/package.json index c44da49..8fbf222 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@types/rox-browser": "^5.0.7", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^7.6.2", "rox-browser": "^5.4.12" }, "devDependencies": { diff --git a/src/About.tsx b/src/About.tsx new file mode 100644 index 0000000..b150cc4 --- /dev/null +++ b/src/About.tsx @@ -0,0 +1,13 @@ +export const About = () => { + return ( +
+

About

+

This is the about page of our application.

+

+ Here you can find information about the application, its features, and + how to use it. +

+

Feel free to explore and learn more!

+
+ ) +} diff --git a/src/App.tsx b/src/App.tsx index 463b135..a2c4639 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,42 +1,63 @@ -import cbLogo from './assets/CB-stacked-logo-full-color.svg' import './App.css' -import {useFeatureFlags} from "./feature-management"; -import {LoadingIndicator} from "./LoadingIndicator.tsx"; +import { Outlet, Route, Routes } from 'react-router-dom' +import { Home } from './Home.tsx' +import { About } from './About.tsx' +import { Nav } from './Nav.tsx' +import { useFeatureFlag } from './useFeatureFlag.ts' +import { namespaceFlags } from './feature-management/flags.ts' -function App() { - - const featureFlags = useFeatureFlags() +export const customRoutes = [ + { + path: '/', + label: 'Home', + element: , + index: true, + featureFlag: { namespace: 'routes', flag: 'home' }, + }, + { + path: 'about', + label: 'About', + element: , + // featureFlag: { namespace: 'routes', flag: 'about' }, + }, +] - if (featureFlags.loading) { - return ( -
- -
- ); - } +const Layout = () => ( +
+
+) +function App() { return ( <> -

CloudBees feature management React sample application

-
- {featureFlags.showMessage.isEnabled() && ( -

- {featureFlags.message.getValue()} -

- )} -
- -
-

- Sign in to the CloudBees platform below to modify flag values and see the changes reflected automatically in this application. -

- - CloudBees logo - -
- + + }> + {customRoutes.map((route, index) => { + const routeFlag = route.featureFlag + ? route.featureFlag.namespace === 'routes' && route.featureFlag.flag === 'about' + ? true + : useFeatureFlag( + namespaceFlags[route.featureFlag.namespace][ + route.featureFlag.flag + ] + ) + : true + return routeFlag ? ( + + ) : null + })} + 404 Not Found} /> + + ) } -export default App +export default App \ No newline at end of file diff --git a/src/Home.tsx b/src/Home.tsx new file mode 100644 index 0000000..ac03c1a --- /dev/null +++ b/src/Home.tsx @@ -0,0 +1,46 @@ +import { useFeatureFlags } from './feature-management' +import { namespaceFlags } from './feature-management/flags' +import { LoadingIndicator } from './LoadingIndicator' +import { useFeatureFlag } from './useFeatureFlag' +import cbLogo from './assets/CB-stacked-logo-full-color.svg' + +export const Home = () => { + const featureFlags = useFeatureFlags() + + const showMessage = useFeatureFlag(namespaceFlags.default.showMessage) + + if (featureFlags.loading) { + return ( +
+ +
+ ) + } + return ( + <> +

CloudBees feature management React sample application

+
+ {showMessage && ( +

+ {featureFlags.default.message.getValue()} +

+ )} +
+ +
+

+ Sign in to the CloudBees platform below to modify flag values and see + the changes reflected automatically in this application. +

+ + CloudBees logo + +
+ + ) +} diff --git a/src/LoadingIndicator.tsx b/src/LoadingIndicator.tsx index bf664ff..eff0556 100644 --- a/src/LoadingIndicator.tsx +++ b/src/LoadingIndicator.tsx @@ -1,9 +1,11 @@ export const LoadingIndicator = () => { - return ( + const showMessage = true // This can be set based on your application's state + + return showMessage ? (
Loading...
- ) -} \ No newline at end of file + ) : null +} diff --git a/src/Nav.tsx b/src/Nav.tsx new file mode 100644 index 0000000..c165ee4 --- /dev/null +++ b/src/Nav.tsx @@ -0,0 +1,29 @@ +// components/NavBar.js +import { Link } from 'react-router-dom' +import { useFeatureFlag } from './useFeatureFlag' +import { customRoutes } from './App' +import { namespaceFlags } from './feature-management/flags' + +export const Nav = () => { + const enabledRoutes = customRoutes.filter((route) => { + const routeFlag = route.featureFlag + ? route.featureFlag.namespace === 'routes' && route.featureFlag.flag === 'about' + ? true + : useFeatureFlag( + namespaceFlags[route.featureFlag.namespace][route.featureFlag.flag] + ) + : true + return routeFlag + }) + + return ( + + ) +} \ No newline at end of file diff --git a/src/feature-management/FeatureFlagsProvider.tsx b/src/feature-management/FeatureFlagsProvider.tsx index 621d713..fc5e530 100644 --- a/src/feature-management/FeatureFlagsProvider.tsx +++ b/src/feature-management/FeatureFlagsProvider.tsx @@ -1,17 +1,16 @@ -import React, {useEffect, useState} from "react"; -import Rox from "rox-browser"; -import {flags} from "./flags.ts"; -import {FeatureFlagsContext, initialFlagState} from "./index.ts"; +import React, { useEffect, useState } from 'react' +import Rox from 'rox-browser' +import { namespaceFlags } from './flags.ts' +import { FeatureFlagsContext, initialFlagState } from './index.ts' // TODO: insert your SDK key from https://cloudbees.io/ below. const sdkKey = '' type Props = { children?: React.ReactNode -}; - -export const FeatureFlagsProvider = ({children} : Props): React.ReactNode => { +} +export const FeatureFlagsProvider = ({ children }: Props): React.ReactNode => { const [flagState, setFlagState] = useState(initialFlagState) const [error, setError] = useState(undefined) @@ -24,44 +23,45 @@ export const FeatureFlagsProvider = ({children} : Props): React.ReactNode => { } initialised.current = true - setFlagState({...flagState, loading: true}) - - Rox.register('', flags) + setFlagState({ ...flagState, loading: true }) - const initFeatureFlags = async() => { + // Register the flags + Object.keys(namespaceFlags).forEach((namespace) => { + const flagsUnderNamespace = (namespaceFlags as any)[namespace] + Rox.register(namespace, flagsUnderNamespace) + }) + const initFeatureFlags = async () => { // Easy to forget to insert your SDK key where shown above, so let's check & remind you! // @ts-ignore if (sdkKey === '') { - throw new Error("You haven't yet inserted your SDK key into FeatureFlagsProvider.tsx - the application below will not update until you do so. Please check the README.adoc for instructions.") + throw new Error() + // "You haven't yet inserted your SDK key into FeatureFlagsProvider.tsx - the application below will not update until you do so. Please check the README.adoc for instructions." } await Rox.setup(sdkKey, { configurationFetchedHandler(fetcherResult: Rox.RoxFetcherResult) { - if (fetcherResult.fetcherStatus === "APPLIED_FROM_NETWORK") { + if (fetcherResult.fetcherStatus === 'APPLIED_FROM_NETWORK') { setFlagState({ ...flagState, }) } - } + }, }) - setFlagState({...flagState, loading: false}) + setFlagState({ ...flagState, loading: false }) } initFeatureFlags().catch((e) => { console.error(e.message) setError(e.message) - setFlagState({...flagState, loading: false}) + setFlagState({ ...flagState, loading: false }) }) - }, [flagState]) return ( - {error && ( -

{error}

- )} + {error &&

{error}

} {children}
diff --git a/src/feature-management/flags.ts b/src/feature-management/flags.ts index f93673f..0f7492d 100644 --- a/src/feature-management/flags.ts +++ b/src/feature-management/flags.ts @@ -1,18 +1,28 @@ -import {Flag, RoxString, RoxNumber} from "rox-browser"; +import { Flag, RoxString, RoxNumber } from 'rox-browser' -type IFeatureFlags = typeof flags +type IFeatureFlags = typeof namespaceFlags export interface IFeatureFlagsState extends IFeatureFlags { - loading: boolean; + loading: boolean } -export const flags = { - // Boolean - should the message be shown? - showMessage: new Flag(), - // String - the message to show. - message: new RoxString('This is the default message; try changing some flag values!'), - // String (with options) - the color of the message text. - fontColor: new RoxString('Black', ['Red', 'Green', 'Blue', 'Black']), - // Number (with options) - the size of the message text. - fontSize: new RoxNumber(12, [12, 16, 24]), -} +export const namespaceFlags = { + namespace: { + namespacedFlag: new Flag(), + }, + routes: { + home: new Flag(true), + }, + default: { + // Boolean - should the message be shown? + showMessage: new Flag(), + // String - the message to show. + message: new RoxString( + 'This is the default message; try changing some flag values!' + ), + // String (with options) - the color of the message text. + fontColor: new RoxString('White', ['Red', 'Green', 'Blue', 'Black']), + // Number (with options) - the size of the message text. + fontSize: new RoxNumber(24, [12, 16, 24]), + }, +} \ No newline at end of file diff --git a/src/feature-management/index.ts b/src/feature-management/index.ts index 76e54e1..a390912 100644 --- a/src/feature-management/index.ts +++ b/src/feature-management/index.ts @@ -1,11 +1,12 @@ -import {createContext, useContext} from "react"; -import {flags, IFeatureFlagsState} from "./flags.ts"; +import { createContext, useContext } from 'react' +import { namespaceFlags, IFeatureFlagsState } from './flags.ts' export const initialFlagState: IFeatureFlagsState = { - ...flags, - loading: false + ...namespaceFlags, + loading: false, } -export const FeatureFlagsContext = createContext(initialFlagState); +export const FeatureFlagsContext = createContext(initialFlagState) -export const useFeatureFlags: () => IFeatureFlagsState = () => useContext(FeatureFlagsContext); \ No newline at end of file +export const useFeatureFlags: () => IFeatureFlagsState = () => + useContext(FeatureFlagsContext) diff --git a/src/main.tsx b/src/main.tsx index f5fec51..cb42f1c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,12 +2,15 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' import './index.css' -import {FeatureFlagsProvider} from "./feature-management/FeatureFlagsProvider.tsx"; +import { FeatureFlagsProvider } from './feature-management/FeatureFlagsProvider.tsx' +import { BrowserRouter } from 'react-router-dom' ReactDOM.createRoot(document.getElementById('root')!).render( - + + + - , + ) diff --git a/src/useFeatureFlag.ts b/src/useFeatureFlag.ts new file mode 100644 index 0000000..b291666 --- /dev/null +++ b/src/useFeatureFlag.ts @@ -0,0 +1,21 @@ +import { useMemo } from 'react' +import { type RoxNumber, type RoxString, Flag } from 'rox-browser' + +type FlagValue = T extends Flag + ? boolean + : T extends RoxString + ? string + : T extends RoxNumber + ? number + : never + +export const useFeatureFlag = ( + flag: T +) => + useMemo>(() => { + if (flag instanceof Flag) { + return flag.isEnabled() as FlagValue + } else { + return flag.getValue() as FlagValue + } + }, [flag]) diff --git a/yarn.lock b/yarn.lock index 7b7cc97..cdfdb01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -836,6 +836,11 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610" + integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== + cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -1500,6 +1505,21 @@ react-refresh@^0.14.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== +react-router-dom@^7.6.2: + version "7.6.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.6.2.tgz#e97e386ab390b6503a2a7968124b7a3237fb10c7" + integrity sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA== + dependencies: + react-router "7.6.2" + +react-router@7.6.2: + version "7.6.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.6.2.tgz#9f48b343bead7d0a94e28342fc4f9ae29131520e" + integrity sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w== + dependencies: + cookie "^1.0.1" + set-cookie-parser "^2.6.0" + react@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" @@ -1580,6 +1600,11 @@ semver@^7.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== +set-cookie-parser@^2.6.0: + version "2.7.1" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943" + integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"