Skip to content

Commit cf3db56

Browse files
committed
chore: allow overwriting toggles in devtools
1 parent a66fab4 commit cf3db56

File tree

5 files changed

+137
-18
lines changed

5 files changed

+137
-18
lines changed

src/devtools/InternalDevTools.tsx

+43
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { DataService } from '../data-fetcher/data-service'
99

1010
import { DevToolItem } from './InternalDevToolItem'
1111
import { useAPIOverride } from './useAPIOverride'
12+
import { useFeatureToggleOverride } from './useFeatureToggleOverride'
13+
import { togglesChangedAction } from './InternalDevToolsAction'
1214

1315
type Props = { onClose: () => void }
1416

@@ -28,13 +30,54 @@ export function InternalDevToolsPanel({ onClose }: Props): ReactElement {
2830
Collection of actions and utilities used for local development only.
2931
</BodyShort>
3032
<div className="grid grid-cols-1 gap-6 mt-6">
33+
<FeatureToggles />
3134
<ToggleAPIFailures />
3235
<ResetSmartContext />
3336
</div>
3437
</div>
3538
)
3639
}
3740

41+
function FeatureToggles(): ReactElement {
42+
const { toggles, toggledToggles, setToggledToggles, resetOverrides } = useFeatureToggleOverride()
43+
44+
return (
45+
<DevToolItem title="Feature toggles" description="Overwrite feature toggles">
46+
<CheckboxGroup
47+
legend="Enabled toggles"
48+
value={toggledToggles}
49+
size="small"
50+
onChange={(values) => {
51+
setToggledToggles(values)
52+
}}
53+
>
54+
{toggles.map((toggle) => (
55+
<Checkbox key={toggle.name} value={toggle.name}>
56+
{toggle.name}
57+
</Checkbox>
58+
))}
59+
</CheckboxGroup>
60+
61+
<div className="mt-4 flex gap-3">
62+
<Button variant="secondary-neutral" size="small" onClick={resetOverrides}>
63+
Clear all overrides
64+
</Button>
65+
<Button
66+
variant="secondary-neutral"
67+
size="small"
68+
onClick={() => {
69+
startTransition(async () => {
70+
await togglesChangedAction()
71+
})
72+
}}
73+
>
74+
Apply changed toggles
75+
</Button>
76+
</div>
77+
</DevToolItem>
78+
)
79+
}
80+
3881
function ResetSmartContext(): ReactElement {
3982
const resetSmartContext = (postAction?: 'reload' | 're-launch') => () => {
4083
const key = sessionStorage.getItem('SMART_KEY')
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use server'
2+
3+
import { revalidatePath } from 'next/cache'
4+
5+
export async function togglesChangedAction(): Promise<void> {
6+
revalidatePath('/fhir')
7+
}
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { useCallback, useMemo, useState } from 'react'
2+
3+
import { localDevelopmentToggles } from '@toggles/env'
4+
5+
type UseFeatureToggleOverride = {
6+
/**
7+
* All available toggles
8+
*/
9+
toggles: { name: string; enabled: boolean }[]
10+
/**
11+
* Currently selected toggles state
12+
*/
13+
toggledToggles: string[]
14+
/**
15+
* Set toggles state and update cookies
16+
*/
17+
setToggledToggles: (value: string[]) => void
18+
/**
19+
* Reset all overrides back to default
20+
*/
21+
resetOverrides: () => void
22+
}
23+
24+
export function useFeatureToggleOverride(): UseFeatureToggleOverride {
25+
const toggles = useMemo(
26+
() =>
27+
localDevelopmentToggles.map((it) => ({
28+
name: it.name,
29+
enabled: getOverrideStatus(it.name) ?? it.enabled,
30+
})),
31+
[],
32+
)
33+
34+
const [toggledToggles, setInternalToggleState] = useState<string[]>(
35+
toggles.filter((it) => it.enabled).map((it) => it.name),
36+
)
37+
38+
const setToggledToggles = useCallback(
39+
(value: string[]) => {
40+
const togglesToEnable = toggles.filter((it) => value.includes(it.name))
41+
const togglesToDisable = toggles.filter((it) => !value.includes(it.name))
42+
43+
togglesToEnable.forEach((it) => {
44+
document.cookie = `${it.name}=true; path=/`
45+
})
46+
togglesToDisable.forEach((it) => {
47+
document.cookie = `${it.name}=false; path=/`
48+
})
49+
50+
setInternalToggleState(value)
51+
},
52+
[toggles],
53+
)
54+
55+
const resetOverrides = useCallback(() => {
56+
toggles.forEach((it) => {
57+
document.cookie = `${it.name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`
58+
})
59+
60+
setInternalToggleState(localDevelopmentToggles.filter((it) => it.enabled).map((it) => it.name))
61+
}, [toggles])
62+
63+
return { toggles, toggledToggles: toggledToggles, setToggledToggles: setToggledToggles, resetOverrides }
64+
}
65+
66+
function getOverrideStatus(key: string): boolean | null {
67+
const currentCookieValue = document.cookie.split(';').find((cookie) => cookie.includes(key))
68+
69+
if (currentCookieValue == null) {
70+
return null
71+
}
72+
73+
return currentCookieValue.includes('true')
74+
}

src/toggles/env.ts

+1-12
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import { IToggle } from '@unleash/nextjs'
44

5-
import { bundledEnv } from '@utils/env'
6-
75
import { ExpectedToggles } from './toggles'
86

97
const on: Omit<IToggle, 'name'> = {
@@ -33,13 +31,4 @@ const devToggles: Record<ExpectedToggles, IToggle> = {
3331
},
3432
}
3533

36-
export function localDevelopmentToggles(): IToggle[] {
37-
return Object.values(devToggles)
38-
}
39-
40-
export function getUnleashEnvironment(): 'development' | 'production' {
41-
if (bundledEnv.NEXT_PUBLIC_RUNTIME_ENV === 'prod-gcp') {
42-
return 'production'
43-
}
44-
return 'development'
45-
}
34+
export const localDevelopmentToggles: IToggle[] = Object.values(devToggles)

src/toggles/unleash.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ import { cookies } from 'next/headers'
55
import NodeCache from 'node-cache'
66
import { connection } from 'next/server'
77

8-
import { isE2E, isLocalOrDemo } from '@utils/env'
8+
import { bundledEnv, isE2E, isLocalOrDemo } from '@utils/env'
99
import { raise } from '@utils/ts'
1010

11-
import { getUnleashEnvironment, localDevelopmentToggles } from './env'
11+
import { localDevelopmentToggles } from './env'
1212
import { EXPECTED_TOGGLES, ExpectedToggles, Toggle, Toggles } from './toggles'
1313
import { UNLEASH_COOKIE_NAME } from './cookie'
1414

1515
const logger = pinoLogger.child({}, { msgPrefix: '[UNLEASH-TOGGLES] ' })
1616

17+
const unleashEnvironment = bundledEnv.NEXT_PUBLIC_RUNTIME_ENV === 'prod-gcp' ? 'production' : 'development'
18+
1719
export async function getToggles(): Promise<Toggles> {
1820
await connection()
1921

@@ -23,19 +25,23 @@ export async function getToggles(): Promise<Toggles> {
2325
}
2426

2527
if (isLocalOrDemo || isE2E) {
26-
const devToggles = localDevelopmentToggles()
2728
logger.warn(
28-
`Running in local or demo mode, falling back to development toggles, current toggles: \n${devToggles.map((it) => `\t${it.name}: ${it.enabled}`).join('\n')}`,
29+
`Running in local or demo mode, falling back to development toggles, current toggles: \n${localDevelopmentToggles.map((it) => `\t${it.name}: ${it.enabled}`).join('\n')}`,
2930
)
30-
return devToggles
31+
32+
const cookieStore = await cookies()
33+
return localDevelopmentToggles.map((it) => ({
34+
...it,
35+
enabled: cookieStore.get(it.name)?.value.includes('true') ?? it.enabled,
36+
}))
3137
}
3238

3339
try {
3440
const sessionId = await getUnleashSessionId()
3541
const definitions = await getAndValidateDefinitions()
3642
const evaluatedFlags = evaluateFlags(definitions, {
3743
sessionId,
38-
environment: getUnleashEnvironment(),
44+
environment: unleashEnvironment,
3945
})
4046
return evaluatedFlags.toggles
4147
} catch (e) {

0 commit comments

Comments
 (0)