Skip to content

Commit

Permalink
chore: feature flag
Browse files Browse the repository at this point in the history
  • Loading branch information
8lane committed Jul 23, 2024
1 parent 763f60c commit e24567d
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 13 deletions.
56 changes: 54 additions & 2 deletions src/app/(fullWidth)/switchboard/(pages)/feature-flags/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function SwitchBoard() {
const cookieStore = cookies()

const {
flags: { 'adverse-weather': adverseWeather },
flags: { 'adverse-weather': adverseWeather, 'map-tile-provider': mapTileProvider },
} = getSwitchBoardState(cookieStore.get(UKHSA_SWITCHBOARD_COOKIE_NAME)?.value)

return (
Expand All @@ -19,7 +19,13 @@ export default function SwitchBoard() {
className="govuk-!-margin-top-3"
action={async (form) => {
'use server'
syncState({}, { 'adverse-weather': form.get('flags.adverseWeather') as string })
syncState(
{},
{
'adverse-weather': form.get('flags.adverseWeather') as string,
'map-tile-provider': form.get('flags.mapTileProvider') as string,
}
)
}}
>
<fieldset className="govuk-fieldset govuk-!-margin-bottom-6">
Expand Down Expand Up @@ -58,6 +64,52 @@ export default function SwitchBoard() {
</label>
</div>
</div>
<div className="govuk-radios govuk-radios--small govuk-!-margin-top-6" data-module="govuk-radios">
<label className="govuk-label w-full" htmlFor="flags.mapTileProvider.Enabled">
Map Tile Provider
</label>
<div className="govuk-radios__item">
<input
defaultChecked={mapTileProvider === 'OpenStreetMaps'}
className="govuk-radios__input"
id="flags.mapTileProvider.OpenStreetMaps"
name="flags.mapTileProvider"
type="radio"
value="OpenStreetMaps"
/>
<label className="govuk-label govuk-radios__label" htmlFor="flags.mapTileProvider.OpenStreetMaps">
Open Street Maps
</label>
</div>

<div className="govuk-radios__item">
<input
defaultChecked={mapTileProvider === 'OrdinanceSurveyMaps'}
className="govuk-radios__input"
id="flags.mapTileProvider.OrdinanceSurveyMaps"
name="flags.mapTileProvider"
type="radio"
value="OrdinanceSurveyMaps"
/>
<label className="govuk-label govuk-radios__label" htmlFor="flags.mapTileProvider.OrdinanceSurveyMaps">
Ordinance Survey Maps
</label>
</div>

<div className="govuk-radios__item">
<input
defaultChecked={mapTileProvider === 'ArcGISEsri'}
className="govuk-radios__input"
id="flags.mapTileProvider.ArcGISEsri"
name="flags.mapTileProvider"
type="radio"
value="ArcGISEsri"
/>
<label className="govuk-label govuk-radios__label" htmlFor="flags.mapTileProvider.ArcGISEsri">
ArcGIS Esri
</label>
</div>
</div>
</fieldset>
<button type="submit" className="govuk-button">
Save changes
Expand Down
1 change: 1 addition & 0 deletions src/app/(fullWidth)/switchboard/shared/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const switchBoardSchema = z.object({
}),
flags: z.object({
'adverse-weather': z.enum(['enabled', 'disabled']),
'map-tile-provider': z.enum(['OpenStreetMaps', 'OrdinanceSurveyMaps', 'ArcGISEsri']),
}),
})

Expand Down
1 change: 1 addition & 0 deletions src/app/(fullWidth)/switchboard/shared/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const initialState: switchBoardSchemaType = {
},
flags: {
'adverse-weather': 'enabled',
'map-tile-provider': 'OrdinanceSurveyMaps',
},
}

Expand Down
4 changes: 2 additions & 2 deletions src/app/components/ui/ukhsa/Map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const mapDefaults: DefaultOptions = {
scrollWheelZoom: true,
attributionControlPosition: 'bottomright',
zoomControlPosition: 'bottomright',
minZoom,
maxZoom,
}

interface MapProps {
Expand All @@ -48,8 +50,6 @@ const Map = ({
<MapContainer
{...options}
id={mapId}
minZoom={minZoom}
maxZoom={maxZoom}
maxBounds={new LatLngBounds([49.528423, -10.76418], [61.331151, 1.9134116])}
ref={ref}
className={clsx('relative overflow-hidden ukhsa-focus', className)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import dynamic from 'next/dynamic'
import { parseAsStringLiteral, useQueryState } from 'nuqs'
import { useMemo } from 'react'
import { useDebounceValue, useWindowSize } from 'usehooks-ts'

import { HealthAlertTypes } from '@/api/models/Alerts'
import {
Expand All @@ -16,16 +17,20 @@ import {
import ExitMapIcon from '@/app/components/ui/ukhsa/Icons/ExitMap'
import { type GeoJSONProps } from '@/app/components/ui/ukhsa/Map/shared/layers/ChoroplethLayer'
import { Skeleton } from '@/app/components/ui/ukhsa/Skeleton/Skeleton'
import { mapQueryKeys } from '@/app/constants/map.constants'
import { center, mapQueryKeys } from '@/app/constants/map.constants'
import useWeatherHealthAlertList from '@/app/hooks/queries/useWeatherHealthAlertList'
import { useTranslation } from '@/app/i18n/client'
import { MapTileProvider } from '@/app/types'

const { Map, BaseLayer, ChoroplethLayer, HealthAlertControl } = {
const { Map, BaseLayer, BaseLayerOSM, ChoroplethLayer, HealthAlertControl } = {
Map: dynamic(() => import('@/app/components/ui/ukhsa/Map/Map'), {
ssr: false,
loading: () => <Skeleton className="h-screen" />,
}),
BaseLayer: dynamic(() => import('@/app/components/ui/ukhsa/Map/shared/layers/BaseLayerOSM'), {
BaseLayer: dynamic(() => import('@/app/components/ui/ukhsa/Map/shared/layers/BaseLayer'), {
ssr: false,
}),
BaseLayerOSM: dynamic(() => import('@/app/components/ui/ukhsa/Map/shared/layers/BaseLayerOSM'), {
ssr: false,
}),
ChoroplethLayer: dynamic(() => import('@/app/components/ui/ukhsa/Map/shared/layers/ChoroplethLayer'), {
Expand All @@ -38,13 +43,17 @@ const { Map, BaseLayer, ChoroplethLayer, HealthAlertControl } = {

interface HealthAlertsMapDialogProps {
featureCollection: GeoJSONProps['data']
mapTileProvider: MapTileProvider
}

export default function HealthAlertsMapDialog({ featureCollection }: HealthAlertsMapDialogProps) {
export default function HealthAlertsMapDialog({ featureCollection, mapTileProvider }: HealthAlertsMapDialogProps) {
const { t } = useTranslation('weatherHealthAlerts')
const [, setError] = useQueryState(mapQueryKeys.error)
const [mapOpen, setMapOpen] = useQueryState(mapQueryKeys.view, parseAsStringLiteral<'map'>(['map']))

const { width } = useWindowSize()
const [debouncedWidth] = useDebounceValue(width, 250)

const [type] = useQueryState(
mapQueryKeys.alertType,
parseAsStringLiteral<HealthAlertTypes>(['heat', 'cold']).withDefault('heat')
Expand All @@ -53,18 +62,22 @@ export default function HealthAlertsMapDialog({ featureCollection }: HealthAlert
const alertsQuery = useWeatherHealthAlertList({ type })

const baseLayer = useMemo(() => {
if (mapTileProvider === 'OrdinanceSurveyMaps') {
return <BaseLayerOSM />
}
return <BaseLayer />
}, [])
}, [mapTileProvider])

const choroplethLayer = useMemo(() => {
if (!alertsQuery.data) return
return (
<ChoroplethLayer
data={featureCollection}
featureColours={Object.fromEntries(alertsQuery.data.map((alert) => [alert.geography_code, alert.status]))}
mapTileProvider={mapTileProvider}
/>
)
}, [featureCollection, alertsQuery.data])
}, [featureCollection, alertsQuery.data, mapTileProvider])

const healthAlertControl = useMemo(() => {
return <HealthAlertControl />
Expand All @@ -77,6 +90,14 @@ export default function HealthAlertsMapDialog({ featureCollection }: HealthAlert
return null
}

const calculateZoom = () => {
if (debouncedWidth < 420) return 6
if (debouncedWidth >= 420 && debouncedWidth < 768) return 6
if (debouncedWidth >= 768 && debouncedWidth < 1024) return 7
if (debouncedWidth >= 1024 && debouncedWidth < 1150) return 8
return 8
}

return (
<Dialog open={!!mapOpen} onOpenChange={(isOpen) => !isOpen && setMapOpen(null)}>
<DialogOverlay />
Expand All @@ -94,7 +115,21 @@ export default function HealthAlertsMapDialog({ featureCollection }: HealthAlert
<DialogTitle>{t('map.title')}</DialogTitle>
</DialogHeader>

<Map>
<Map
key={debouncedWidth} // Force Re-render the map when the window width changes (i.e. browser resize or device orientation change)
options={
mapTileProvider === 'OrdinanceSurveyMaps'
? {
center,
zoom: calculateZoom(),
minZoom: calculateZoom(),
maxZoom: 12,
attributionControlPosition: 'bottomright',
zoomControlPosition: 'bottomright',
}
: undefined
}
>
{baseLayer}
{choroplethLayer}
{healthAlertControl}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,22 @@ const HealthAlertsMapDialog = dynamic(() => import('./HealthAlertsMapDialog'))

import { ErrorBoundary } from 'react-error-boundary'

import { flags } from '@/app/constants/flags.constants'
import { getFeatureFlag } from '@/app/utils/flags.utils'

export async function HealthAlertsMapWrapper() {
const {
variant: { name: mapTileProvider },
} = await getFeatureFlag(flags.mapTileProvider)

return (
<ErrorBoundary fallback={null}>
<Suspense fallback={null}>
<HealthAlertsMapDialog featureCollection={featureCollection} />
{mapTileProvider === 'OrdinanceSurveyMaps' ? (
<HealthAlertsMapDialog mapTileProvider="OrdinanceSurveyMaps" featureCollection={featureCollection} />
) : (
<HealthAlertsMapDialog mapTileProvider="OpenStreetMaps" featureCollection={featureCollection} />
)}
</Suspense>
</ErrorBoundary>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { GeoJSON, useMapEvents } from 'react-leaflet'

import { HealthAlertStatus } from '@/api/models/Alerts'
import { geoJsonFeatureId, mapQueryKeys } from '@/app/constants/map.constants'
import { MapTileProvider } from '@/app/types'
import {
getActiveCssVariableFromColour,
getCssVariableFromColour,
Expand Down Expand Up @@ -60,6 +61,11 @@ interface ChoroplethProps extends Omit<GeoJSONProps, 'data'> {
* Optional class name to attach to each feature.
*/
className?: string

/**
* Optional class name to attach to each feature.
*/
mapTileProvider: MapTileProvider
}

interface CustomLeafletEvent extends LeafletMouseEvent {
Expand Down
1 change: 1 addition & 0 deletions src/app/constants/flags.constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const flags = {
weatherHealthAlert: 'adverse-weather',
mapTileProvider: 'map-tile-provider',
}
10 changes: 10 additions & 0 deletions src/app/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* CMS Page generation types
*/

export type Slug = string[]

export interface PageParams {
Expand All @@ -10,3 +14,9 @@ export interface PageComponentBaseProps<S extends Record<string, unknown> = Reco
slug: Slug
searchParams: Partial<S>
}

/**
* Mapping types
*/

export type MapTileProvider = 'OpenStreetMaps' | 'ArcGISEsri' | 'OrdinanceSurveyMaps' | string
13 changes: 12 additions & 1 deletion src/mock-server/handlers/flags/client/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@ export default async function handler(req: Request, res: Response) {
...featureFlags,
features: featureFlags.features.map((feature) => {
if (feature.name in flags) {
return { ...feature, enabled: flags[feature.name as keyof typeof flags] === 'enabled' }
return {
...feature,
enabled: flags[feature.name as keyof typeof flags] !== 'disabled',
variants: feature.variants
.map((variant) => ({
...variant,
enabled: flags[feature.name as keyof typeof flags] === variant.name,
feature_enabled: flags[feature.name as keyof typeof flags] === variant.name,
}))
// Unleash takes the first array item regardless of whether enabled is true or false.
.sort((first, second) => (first.enabled === second.enabled ? 0 : first.enabled ? -1 : 1)),
}
}
}),
})
Expand Down
30 changes: 30 additions & 0 deletions src/mock-server/handlers/flags/client/fixtures/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,36 @@ export const featureFlags = {
description: null,
impressionData: false,
},
{
name: 'map-tile-provider',
type: 'release',
enabled: true,
project: 'default',
stale: false,
strategies: [],
variants: [
{
name: 'OrdinanceSurveyMaps',
enabled: true,
payload: { type: 'string', value: 'OrdinanceSurveyMaps' },
feature_enabled: true,
},
{
name: 'OpenStreetMaps',
enabled: false,
payload: { type: 'string', value: 'OpenStreetMaps' },
feature_enabled: false,
},
{
name: 'ArcGISEsri',
enabled: false,
payload: { type: 'string', value: 'ArcGISEsri' },
feature_enabled: false,
},
],
description: null,
impressionData: false,
},
],
query: {
inlineSegmentConstraints: true,
Expand Down

0 comments on commit e24567d

Please sign in to comment.