Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synchronous catalog config from a global var + types #3166

Merged
merged 13 commits into from
Nov 1, 2022
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 { getGlobalConfig } from 'utils/Config'

export default getGlobalConfig()
fiskus marked this conversation as resolved.
Show resolved Hide resolved
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.

123 changes: 123 additions & 0 deletions catalog/app/utils/Config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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 getConfig(input: unknown) {
fiskus marked this conversation as resolved.
Show resolved Hide resolved
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 getConfig>

export function getGlobalConfig(configKey: string = 'QUILT_CATALOG_CONFIG') {
const rawConfig = (window as any)[configKey]
invariant(rawConfig, `window.${configKey} must be defined`)
return getConfig(rawConfig)
}

function cfgMemo(container: any, key: string = '__cfg') {
if (!(key in container)) {
container[key] = getGlobalConfig()
}
return container[key]
}

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 = cfgMemo(useConfig)
if (opts?.suspend === false) return { promise: Promise.resolve(cfg) }
return cfg
}

export { useConfig as use }
Loading