Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
13 changes: 13 additions & 0 deletions src/About.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const About = () => {
return (
<div className="container">
<h1>About</h1>
<p>This is the about page of our application.</p>
<p>
Here you can find information about the application, its features, and
how to use it.
</p>
<p>Feel free to explore and learn more!</p>
</div>
)
}
85 changes: 53 additions & 32 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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: <Home />,
index: true,
featureFlag: { namespace: 'routes', flag: 'home' },
},
{
path: 'about',
label: 'About',
element: <About />,
// featureFlag: { namespace: 'routes', flag: 'about' },
},
]

if (featureFlags.loading) {
return (
<div className="position-relative pb-9">
<LoadingIndicator />
</div>
);
}
const Layout = () => (
<div>
<Nav />
<Outlet />
</div>
)

function App() {
return (
<>
<h1>CloudBees feature management React sample application</h1>
<div className="card">
{featureFlags.showMessage.isEnabled() && (
<p style={{color: featureFlags.fontColor.getValue(), fontSize: featureFlags.fontSize.getValue()}}>
{featureFlags.message.getValue()}
</p>
)}
</div>

<div className="card">
<p className="access-platform">
Sign in to the CloudBees platform below to modify flag values and see the changes reflected automatically in this application.
</p>
<a href="https://cloudbees.io" target="_blank">
<img src={cbLogo} className="logo" alt="CloudBees logo"/>
</a>
</div>

<Routes>
<Route path="/" element={<Layout />}>
{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 ? (
<Route
key={index}
path={route.path}
element={route.element}
index={route.index}
/>
) : null
})}
<Route path="*" element={<div>404 Not Found</div>} />
</Route>
</Routes>
</>
)
}

export default App
export default App
46 changes: 46 additions & 0 deletions src/Home.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="position-relative pb-9">
<LoadingIndicator />
</div>
)
}
return (
<>
<h1>CloudBees feature management React sample application</h1>
<div className="card">
{showMessage && (
<p
style={{
color: featureFlags.default.fontColor.getValue(),
fontSize: featureFlags.default.fontSize.getValue(),
}}
>
{featureFlags.default.message.getValue()}
</p>
)}
</div>

<div className="card">
<p className="access-platform">
Sign in to the CloudBees platform below to modify flag values and see
the changes reflected automatically in this application.
</p>
<a href="https://cloudbees.io" target="_blank">
<img src={cbLogo} className="logo" alt="CloudBees logo" />
</a>
</div>
</>
)
}
8 changes: 5 additions & 3 deletions src/LoadingIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export const LoadingIndicator = () => {
return (
const showMessage = true // This can be set based on your application's state

return showMessage ? (
<div className="position-absolute top-50 start-50 translate-middle">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
)
}
) : null
}
29 changes: 29 additions & 0 deletions src/Nav.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<nav>
{enabledRoutes.map((route, index) => (
<div key={index} style={{ display: 'inline' }}>
<Link to={route.path}>{route.label}</Link>
{index < enabledRoutes.length - 1 && <span> | </span>}
</div>
))}
</nav>
)
}
40 changes: 20 additions & 20 deletions src/feature-management/FeatureFlagsProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 = '<YOUR-SDK-KEY>'

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<string | undefined>(undefined)

Expand All @@ -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 === '<YOUR-SDK-KEY>') {
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 (
<FeatureFlagsContext.Provider value={flagState}>
{error && (
<h3 style={{color: 'red'}}>{error}</h3>
)}
{error && <h3 style={{ color: 'red' }}>{error}</h3>}

{children}
</FeatureFlagsContext.Provider>
Expand Down
36 changes: 23 additions & 13 deletions src/feature-management/flags.ts
Original file line number Diff line number Diff line change
@@ -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]),
},
}
13 changes: 7 additions & 6 deletions src/feature-management/index.ts
Original file line number Diff line number Diff line change
@@ -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);
export const useFeatureFlags: () => IFeatureFlagsState = () =>
useContext(FeatureFlagsContext)
9 changes: 6 additions & 3 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<React.StrictMode>
<FeatureFlagsProvider>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</FeatureFlagsProvider>
</React.StrictMode>,
</React.StrictMode>
)
21 changes: 21 additions & 0 deletions src/useFeatureFlag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useMemo } from 'react'
import { type RoxNumber, type RoxString, Flag } from 'rox-browser'

type FlagValue<T> = T extends Flag
? boolean
: T extends RoxString
? string
: T extends RoxNumber
? number
: never

export const useFeatureFlag = <T extends Flag | RoxString | RoxNumber>(
flag: T
) =>
useMemo<FlagValue<T>>(() => {
if (flag instanceof Flag) {
return flag.isEnabled() as FlagValue<T>
} else {
return flag.getValue() as FlagValue<T>
}
}, [flag])
Loading