Skip to content

Commit

Permalink
feat: load modules based on remoteConfig
Browse files Browse the repository at this point in the history
Remote Config is a seperate product within the firebase platform
which allows users to store state remotely which can be edited
outside of deployments.
Link: https://firebase.google.com/docs/remote-config

Using remote configuration we are then able to defer
module configuration out to the target environment.

This commit adds a check against remote config when:
1. Building the desktop menu
2. Registering routes
3. Example of checking module support within a Profile page

Questions which still require further investigation:
* Loading state when fetching remote configuration
* Use mobx as a wrapper for this Firebase product

Request for comment:
* A new src/modules/ directory to serve as the primary
entry point for interacting with the Module concept.
  • Loading branch information
thisislawatts committed Nov 8, 2021
1 parent 887ff58 commit c006e25
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 90 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
FAST_REFRESH=false
SITE_NAME=Project Kamp
REACT_APP_SITE_VARIANT=dev_site
136 changes: 63 additions & 73 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,71 +47,6 @@ const branch = e.REACT_APP_BRANCH as string
// On dev sites user can override default role
const devSiteRole: UserRole = localStorage.getItem('devSiteRole') as UserRole

function getSiteVariant(
gitBranch: string,
env: typeof process.env,
): SiteVariants {
console.log({
env,
})
const devSiteVariant: SiteVariants = localStorage.getItem(
'devSiteVariant',
) as any
if (devSiteVariant === 'preview') {
return 'preview'
}
if (devSiteVariant === 'emulated_site') {
return 'emulated_site'
}
if (devSiteVariant === 'dev_site') {
return 'dev_site'
}
if (location.host === 'localhost:4000') {
return 'emulated_site'
}
if (env.REACT_APP_SITE_VARIANT === 'test-ci') {
return 'test-ci'
}
if (env.REACT_APP_SITE_VARIANT === 'preview') {
return 'preview'
}
switch (gitBranch) {
case 'production':
return 'production'
case 'master':
return 'staging'
default:
return 'dev_site'
}
}

const siteVariant = getSiteVariant(branch, e)
console.log(`[${siteVariant}] site`)

/*********************************************************************************************** /
Production
/********************************************************************************************** */

// production config is passed as environment variables during CI build.
if (siteVariant === 'production') {
// note, technically not required as supplied directly to firebase config() method during build
sentryConfig = {
dsn: e.REACT_APP_SENTRY_DSN as string,
}
// TODO - create production algolia config
algoliaSearchConfig = {
applicationID: '',
searchOnlyAPIKey: '',
}
algoliaPlacesConfig = {
applicationID: e.REACT_APP_ALGOLIA_PLACES_APP_ID as string,
searchOnlyAPIKey: e.REACT_APP_ALGOLIA_PLACES_API_KEY as string,
}
// disable console logs
// eslint-disable-next-line
console.log = () => {}
}

const firebaseConfigs: { [variant in SiteVariants]: IFirebaseConfig } = {
/** Sandboxed dev site, all features available for interaction */
dev_site: {
Expand All @@ -120,7 +55,7 @@ const firebaseConfigs: { [variant in SiteVariants]: IFirebaseConfig } = {
projectId: 'la-project-kamp-development',
storageBucket: 'la-project-kamp-development.appspot.com',
messagingSenderId: '868509486863',
databaseURL: 'https://la-project-kamp-development.firebaseio.com',
databaseURL: 'https://la-project-kamp-development.europe-west1.firebasedatabase.app',
appId: '1:868509486863:web:e24ef72c814800a43cb87a',
},
beta_dev_site: {
Expand Down Expand Up @@ -176,15 +111,70 @@ const firebaseConfigs: { [variant in SiteVariants]: IFirebaseConfig } = {
},
}

/*
function getSiteVariant(
gitBranch: string,
env: typeof process.env,
): SiteVariants {
console.log({
env,
})

What we want is load our config from the env variables
rather than using this confused configuration file.
if (Object.keys(firebaseConfigs).includes(env.REACT_APP_SITE_VARIANT || '')) {
return env.REACT_APP_SITE_VARIANT as SiteVariants;
}

The variables
const devSiteVariant: SiteVariants = localStorage.getItem(
'devSiteVariant',
) as SiteVariants

http://la-project-kamp-development.firebaseio.com/
*/
if (devSiteVariant === 'preview') {
return 'preview'
}
if (devSiteVariant === 'emulated_site') {
return 'emulated_site'
}
if (devSiteVariant === 'dev_site') {
return 'dev_site'
}
if (location.host === 'localhost:4000') {
return 'emulated_site'
}
switch (gitBranch) {
case 'production':
return 'production'
case 'master':
return 'staging'
default:
return 'dev_site'
}
}

const siteVariant = getSiteVariant(branch, e)
console.log(`[${siteVariant}] site`)

/*********************************************************************************************** /
Production
/********************************************************************************************** */

// production config is passed as environment variables during CI build.
if (siteVariant === 'production') {
// note, technically not required as supplied directly to firebase config() method during build
sentryConfig = {
dsn: e.REACT_APP_SENTRY_DSN as string,
}
// TODO - create production algolia config
algoliaSearchConfig = {
applicationID: '',
searchOnlyAPIKey: '',
}
algoliaPlacesConfig = {
applicationID: e.REACT_APP_ALGOLIA_PLACES_APP_ID as string,
searchOnlyAPIKey: e.REACT_APP_ALGOLIA_PLACES_API_KEY as string,
}
// disable console logs
// eslint-disable-next-line
console.log = () => {}
}

/*********************************************************************************************** /
Exports
Expand All @@ -197,4 +187,4 @@ export const ALGOLIA_SEARCH_CONFIG = algoliaSearchConfig
export const ALGOLIA_PLACES_CONFIG = algoliaPlacesConfig
export const SENTRY_CONFIG = sentryConfig
export const VERSION = require('../../package.json').version
export const GA_TRACKING_ID = process.env.REACT_APP_GA_TRACKING_ID
export const GA_TRACKING_ID = process.env.REACT_APP_GA_TRACKING_ID
27 changes: 27 additions & 0 deletions src/modules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { remoteConfig } from 'src/utils/firebase'

export async function getSupportedModules(): Promise<string[]> {
await remoteConfig.fetchAndActivate()
return JSON.parse(remoteConfig.getValue('enabledModules').asString())
}

export enum MODULE {
HOWTO = 'howto',
MAP = 'map',
EVENTS = 'events',
RESEARCH = 'research',
ACADEMY = 'academy',
}

/**
* The default configuration which will be used
* if no configuration options for `enabledModules` is
* found on the remote configuration associated with the
* current firebase project.
*/
remoteConfig.defaultConfig.enabledModules = JSON.stringify([
MODULE.MAP,
MODULE.HOWTO,
MODULE.EVENTS,
MODULE.ACADEMY,
])
16 changes: 15 additions & 1 deletion src/pages/PageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Route } from 'react-router'
import { UserRole } from 'src/models/user.models'
import ExternalEmbed from 'src/components/ExternalEmbed/ExternalEmbed'
import { ResearchModule } from './Research'
import { getSupportedModules, MODULE } from 'src/modules'


/**
* Import all pages for use in lazy loading
Expand Down Expand Up @@ -48,6 +50,7 @@ const howTo = {
component: <HowtoPage />,
title: 'How-to',
description: 'Welcome to how-to',
moduleName: MODULE.HOWTO,
}
const settings = {
path: '/settings',
Expand Down Expand Up @@ -80,12 +83,14 @@ const academy = {
flex: 1,
},
fullPageWidth: true,
moduleName: MODULE.ACADEMY,
}
const events = {
path: '/events',
component: <EventsPage />,
title: 'Events',
description: 'Welcome to Events',
moduleName: MODULE.EVENTS,
}
const maps = {
path: '/map',
Expand All @@ -99,6 +104,7 @@ const maps = {
width: '100vw',
},
fullPageWidth: true,
moduleName: MODULE.MAP,
}
const admin = {
path: '/admin',
Expand Down Expand Up @@ -161,12 +167,20 @@ const termsPolicy = {
description: '',
}

export async function getAvailablePageList(): Promise<IPageMeta[]> {
const enabledModules: string[] = await getSupportedModules()

return COMMUNITY_PAGES.filter(pageItem =>
enabledModules.includes(pageItem.moduleName || ''),
)
}

// community pages (various pages hidden on production build)
export const COMMUNITY_PAGES: IPageMeta[] = ['preview', 'production'].includes(
SITE,
)
? [howTo, maps, events, academy]
: [academy, ResearchModule] // [howTo, maps, events, academy, ResearchModule]
: [howTo, maps, events, academy, ResearchModule]

export const COMMUNITY_PAGES_MORE: IPageMeta[] = []
export const COMMUNITY_PAGES_PROFILE: IPageMeta[] = [settings]
Expand Down
12 changes: 11 additions & 1 deletion src/pages/Settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import INITIAL_VALUES from './Template'
import { Box } from 'rebass'
import { Prompt } from 'react-router'
import { toJS } from 'mobx'
import { getSupportedModules, MODULE } from 'src/modules'

interface IProps {}

Expand All @@ -32,6 +33,7 @@ interface IInjectedProps extends IProps {
interface IState {
formValues: IUserPP
notification: { message: string; icon: string; show: boolean }
supportedModules?: string[]
showDeleteDialog?: boolean
}

Expand Down Expand Up @@ -96,6 +98,12 @@ export class UserSettings extends React.Component<IProps, IState> {
}
}

public async componentDidMount() {
this.setState({
supportedModules: await getSupportedModules(),
})
}

/**
* Check for additional erros not caught by standard validation
* Return any errors as json object
Expand Down Expand Up @@ -183,7 +191,9 @@ export class UserSettings extends React.Component<IProps, IState> {
<ProfileGuidelines />
</Box>
{/* Note - for fields without fieldwrapper can just render via props method and bind to input */}
<FocusSection />
{this.state.supportedModules?.includes(MODULE.MAP) && (
<FocusSection />
)}
{/* Specific profile type fields */}
{values.profileType === 'workspace' && (
<WorkspaceSection />
Expand Down
18 changes: 15 additions & 3 deletions src/pages/common/Header/Menu/MenuDesktop.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component } from 'react'
import { NavLink } from 'react-router-dom'
import { COMMUNITY_PAGES } from 'src/pages/PageList'
import { getAvailablePageList } from 'src/pages/PageList'
import theme from 'src/themes/styled.theme'
import { Flex } from 'rebass/styled-components'
import styled from 'styled-components'
Expand Down Expand Up @@ -40,11 +40,21 @@ const MenuLink = styled(NavLink).attrs(() => ({
`

export class MenuDesktop extends Component {
state = {
menuItems: null,
}

componentDidMount() {
getAvailablePageList().then(menuItems => {
this.setState({menuItems});
})
}

render() {
return (
return this.state.menuItems ? (
<>
<Flex alignItems={'center'}>
{COMMUNITY_PAGES.map(page => {
{((this.state.menuItems || []) as any[]).map((page :any) => {
const link = (
<Flex key={page.path}>
<MenuLink to={page.path} data-cy="page-link">
Expand All @@ -62,6 +72,8 @@ export class MenuDesktop extends Component {
})}
</Flex>
</>
) : (
'Loading'
)
}
}
Expand Down
Loading

0 comments on commit c006e25

Please sign in to comment.