Skip to content

Commit

Permalink
Synchronous catalog config from a global var + types (#3166)
Browse files Browse the repository at this point in the history
  • Loading branch information
nl0 authored Nov 1, 2022
1 parent 6d50ef5 commit 9b2b74c
Show file tree
Hide file tree
Showing 18 changed files with 175 additions and 128 deletions.
6 changes: 4 additions & 2 deletions catalog/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ COPY build /usr/share/nginx/html
# Copy config file
COPY config.json.tmpl config.json.tmpl

# Substitute environment variables into config.js before starting nginx.
# Substitute environment variables into config.json and generate config.js based on that before starting nginx.
# Note: use "exec" because otherwise the shell will catch Ctrl-C and other signals.
CMD envsubst < config.json.tmpl > /usr/share/nginx/html/config.json && exec nginx -g 'daemon off;'
CMD envsubst < config.json.tmpl > /usr/share/nginx/html/config.json \
&& echo "window.QUILT_CATALOG_CONFIG = `cat /usr/share/nginx/html/config.json`" > /usr/share/nginx/html/config.js \
&& exec nginx -g 'daemon off;'
6 changes: 3 additions & 3 deletions catalog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ The catalog is a web frontend for browsing meta-data held by the Quilt registry.
# Developer
## Configuration
The app configuration (API endpoints, bucket federations, etc.) is read from
the `/config.json` path.
the `/config.js` path.

## Running the catalog locally
```sh
# local web server for catalog w/hot reload
$ cd catalog
# copy and edit config file
$ cp config.json.example static-dev/config.json
$ vi static-dev/config.json
$ cp config.js.example static-dev/config.js
$ vi static-dev/config.js
$ npm start
```

Expand Down
2 changes: 0 additions & 2 deletions catalog/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import * as AWS from 'utils/AWS'
import * as APIConnector from 'utils/APIConnector'
import { GraphQLProvider } from 'utils/GraphQL'
import { BucketCacheProvider } from 'utils/BucketCache'
import * as Config from 'utils/Config'
import GlobalAPI from 'utils/GlobalAPI'
import * as NamedRoutes from 'utils/NamedRoutes'
import * as Cache from 'utils/ResourceCache'
Expand Down Expand Up @@ -89,7 +88,6 @@ const render = () => {
[NamedRoutes.Provider, { routes }],
[RouterProvider, { history }],
Cache.Provider,
[Config.Provider, { path: '/config.json' }],
[React.Suspense, { fallback: <Placeholder /> }],
[Sentry.Loader, { userSelector: sentryUserSelector }],
GraphQLProvider,
Expand Down
3 changes: 3 additions & 0 deletions catalog/app/constants/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { getConfig } from 'utils/Config'

export default getConfig()
2 changes: 0 additions & 2 deletions catalog/app/embed/Embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import * as style from 'constants/style'
import * as APIConnector from 'utils/APIConnector'
import * as AWS from 'utils/AWS'
import * as BucketCache from 'utils/BucketCache'
import * as Config from 'utils/Config'
import { createBoundary } from 'utils/ErrorBoundary'
import { GraphQLProvider } from 'utils/GraphQL'
import * as NamedRoutes from 'utils/NamedRoutes'
Expand Down Expand Up @@ -332,7 +331,6 @@ function App({ init }) {
[Store.Provider, { history }],
[RouterProvider, { history }],
Cache.Provider,
[Config.Provider, { path: '/config.json' }],
[React.Suspense, { fallback: <Placeholder color="text.secondary" /> }],
GraphQLProvider,
Notifications.Provider,
Expand Down
4 changes: 3 additions & 1 deletion catalog/app/embed/debug-harness.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
<script src="https://polyfill.io/v3/polyfill.min.js?features=Intl%2Cdefault%2CResizeObserver%2CBlob%2Cfetch%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2015%2CIntl.RelativeTimeFormat"></script>
<script defer src="https://polyfill.io/v3/polyfill.min.js?features=Intl%2Cdefault%2CResizeObserver%2CBlob%2Cfetch%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2015%2CIntl.RelativeTimeFormat"></script>
<!-- webpackIgnore: true -->
<script defer src="/config.js"></script>
</head>
<body>
<!--[if lte IE 10]>
Expand Down
1 change: 0 additions & 1 deletion catalog/app/embed/debug-harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,6 @@ function App() {
Sentry.Provider,
Store.Provider,
Cache.Provider,
[Config.Provider, { path: '/config.json' }],
[React.Suspense, { fallback: <Placeholder color="text.secondary" /> }],
Embedder,
)
Expand Down
4 changes: 3 additions & 1 deletion catalog/app/embed/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
<script src="https://polyfill.io/v3/polyfill.min.js?features=Intl%2Cdefault%2CResizeObserver%2CBlob%2Cfetch%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2015%2CIntl.RelativeTimeFormat"></script>
<script defer src="https://polyfill.io/v3/polyfill.min.js?features=Intl%2Cdefault%2CResizeObserver%2CBlob%2Cfetch%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2015%2CIntl.RelativeTimeFormat"></script>
<!-- webpackIgnore: true -->
<script defer src="/config.js"></script>
</head>
<body>
<!--[if lte IE 10]>
Expand Down
4 changes: 3 additions & 1 deletion catalog/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
<script src="https://polyfill.io/v3/polyfill.min.js?features=Intl%2Cdefault%2CResizeObserver%2CBlob%2Cfetch%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2015%2CIntl.RelativeTimeFormat"></script>
<script defer src="https://polyfill.io/v3/polyfill.min.js?features=Intl%2Cdefault%2CResizeObserver%2CBlob%2Cfetch%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2015%2CIntl.RelativeTimeFormat"></script>
<!-- webpackIgnore: true -->
<script defer src="/config.js"></script>
</head>
<body>
<!--[if lte IE 10]>
Expand Down
101 changes: 0 additions & 101 deletions catalog/app/utils/Config.js

This file was deleted.

121 changes: 121 additions & 0 deletions catalog/app/utils/Config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import Ajv from 'ajv'
import invariant from 'invariant'

import { BaseError } from 'utils/error'
import { printObject } from 'utils/string'

import configSchema from '../../config-schema.json'

type Mode = 'MARKETING' | 'OPEN' | 'PRODUCT' | 'LOCAL'
type AuthMethodConfig = 'ENABLED' | 'DISABLED' | 'SIGN_IN_ONLY'

// manually synced w/ config-schema.json
export interface ConfigJson {
alwaysRequiresAuth: boolean
analyticsBucket?: string
apiGatewayEndpoint: string
binaryApiGatewayEndpoint: string
calendlyLink?: string
googleClientId?: string
oktaClientId?: string
oktaBaseUrl?: string
intercomAppId?: string
mode: Mode
legacyPackagesRedirect?: string
linkedData?: {
name?: string
description?: string
}
mixpanelToken: string
noDownload?: boolean
noOverviewImages?: boolean
passwordAuth: AuthMethodConfig
registryUrl: string
s3Proxy: string
sentryDSN?: string
serviceBucket: string
ssoAuth: AuthMethodConfig
ssoProviders: string
desktop?: boolean
build_version?: string // not sure where this comes from
}

const ajv = new Ajv({ allErrors: true, removeAdditional: true })

ajv.addSchema(configSchema, 'Config')

export class ConfigError extends BaseError {
static displayName = 'ConfigError'
}

function validateConfig(input: unknown): asserts input is ConfigJson {
if (ajv.validate('Config', input)) return
throw new ConfigError(
[
'invalid config format:',
ajv.errorsText(),
'Errors array:',
printObject(ajv.errors),
'Input:',
printObject(input),
].join('\n'),
{ errors: ajv.errors, input },
)
}

const AUTH_MAP = {
ENABLED: true,
DISABLED: false,
SIGN_IN_ONLY: 'SIGN_IN_ONLY',
}

const startWithOrigin = (s: string) => (s.startsWith('/') ? window.origin + s : s)

const transformConfig = (cfg: ConfigJson) => ({
...cfg,
passwordAuth: AUTH_MAP[cfg.passwordAuth],
ssoAuth: AUTH_MAP[cfg.ssoAuth],
ssoProviders: cfg.ssoProviders.length ? cfg.ssoProviders.split(' ') : [],
enableMarketingPages: cfg.mode === 'PRODUCT' || cfg.mode === 'MARKETING',
disableNavigator: cfg.mode === 'MARKETING',
s3Proxy: startWithOrigin(cfg.s3Proxy),
apiGatewayEndpoint: startWithOrigin(cfg.apiGatewayEndpoint),
binaryApiGatewayEndpoint: startWithOrigin(cfg.binaryApiGatewayEndpoint),
noDownload: !!cfg.noDownload,
noOverviewImages: !!cfg.noOverviewImages,
desktop: !!cfg.desktop,
})

export function prepareConfig(input: unknown) {
try {
validateConfig(input)
return transformConfig(input)
} catch (e) {
if (e instanceof ConfigError) throw e
throw new ConfigError('unexpected error', { originalError: e })
}
}

export type Config = ReturnType<typeof prepareConfig>

let cachedConfig: Config | null = null
const configKey = 'QUILT_CATALOG_CONFIG'
export function getConfig() {
if (!cachedConfig) {
const rawConfig = (window as any)[configKey]
invariant(rawConfig, `window.${configKey} must be defined`)
cachedConfig = prepareConfig(rawConfig)
}
return cachedConfig
}

export function useConfig(opts: { suspend: false }): { promise: Promise<Config> }
export function useConfig(): Config
/** @deprecated Config is now synchronous -- just import 'constants/config' module directly */
export function useConfig(opts?: { suspend?: boolean }) {
const cfg = getConfig()
if (opts?.suspend === false) return { promise: Promise.resolve(cfg) }
return cfg
}

export { useConfig as use }
Loading

0 comments on commit 9b2b74c

Please sign in to comment.