Skip to content

Commit

Permalink
🪟 🔧 Refactor webapp configuration provider (#21456)
Browse files Browse the repository at this point in the history
  • Loading branch information
josephkmh authored Jan 27, 2023
1 parent 5ceb14a commit 502e1b0
Show file tree
Hide file tree
Showing 31 changed files with 159 additions and 361 deletions.
14 changes: 3 additions & 11 deletions airbyte-webapp/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ThemeProvider } from "styled-components";

import { ApiErrorBoundary } from "components/common/ApiErrorBoundary";

import { config } from "config";
import { ApiServices } from "core/ApiServices";
import { I18nProvider } from "core/i18n";
import { ServicesProvider } from "core/servicesProvider";
Expand All @@ -18,14 +19,7 @@ import { AnalyticsProvider } from "views/common/AnalyticsProvider";
import { StoreProvider } from "views/common/StoreProvider";

import LoadingPage from "./components/LoadingPage";
import {
Config,
ConfigServiceProvider,
defaultConfig,
envConfigProvider,
ValueProvider,
windowConfigProvider,
} from "./config";
import { ConfigServiceProvider } from "./config";
import en from "./locales/en.json";
import { Routing } from "./pages/routes";
import { WorkspaceServiceProvider } from "./services/workspaces/WorkspacesService";
Expand All @@ -35,8 +29,6 @@ const StyleProvider: React.FC<React.PropsWithChildren<unknown>> = ({ children })
<ThemeProvider theme={theme}>{children}</ThemeProvider>
);

const configProviders: ValueProvider<Config> = [envConfigProvider, windowConfigProvider];

const Services: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => (
<AnalyticsProvider>
<AppMonitoringServiceProvider>
Expand Down Expand Up @@ -69,7 +61,7 @@ const App: React.FC = () => {
<StoreProvider>
<ServicesProvider>
<Suspense fallback={<LoadingPage />}>
<ConfigServiceProvider defaultConfig={defaultConfig} providers={configProviders}>
<ConfigServiceProvider config={config}>
<Router>
<Services>
<Routing />
Expand Down
37 changes: 10 additions & 27 deletions airbyte-webapp/src/config/ConfigServiceProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,27 @@
import React, { useContext, useMemo } from "react";
import { useAsync } from "react-use";
import React, { useContext } from "react";

import { LoadingPage } from "components";
import { AirbyteWebappConfig } from "./types";

import { applyProviders } from "./configProviders";
import { Config, ValueProvider } from "./types";

export interface ConfigContextData<T extends Config = Config> {
config: T;
export interface ConfigContextData {
config: AirbyteWebappConfig;
}

export const ConfigContext = React.createContext<ConfigContextData | null>(null);

export function useConfig<T extends Config>(): T {
export function useConfig(): AirbyteWebappConfig {
const configService = useContext(ConfigContext);

if (configService === null) {
throw new Error("useConfig must be used within a ConfigProvider");
}

return useMemo(() => configService.config as unknown as T, [configService.config]);
return configService.config;
}

const ConfigServiceInner: React.FC<
export const ConfigServiceProvider: React.FC<
React.PropsWithChildren<{
defaultConfig: Config;
providers?: ValueProvider<Config>;
config: AirbyteWebappConfig;
}>
> = ({ children, defaultConfig, providers }) => {
const { loading, value } = useAsync(
async () => (providers ? applyProviders(defaultConfig, providers) : defaultConfig),
[providers]
);
const config: ConfigContextData | null = useMemo(() => (value ? { config: value } : null), [value]);

if (loading) {
return <LoadingPage />;
}

return <ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>;
> = ({ children, config }) => {
return <ConfigContext.Provider value={{ config }}>{children}</ConfigContext.Provider>;
};

export const ConfigServiceProvider = React.memo(ConfigServiceInner);
39 changes: 39 additions & 0 deletions airbyte-webapp/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { AirbyteWebappConfig } from "./types";

export const config: AirbyteWebappConfig = {
segment: {
token: window.SEGMENT_TOKEN ?? process.env.REACT_APP_SEGMENT_TOKEN,
enabled: window.TRACKING_STRATEGY === "segment",
},
apiUrl: window.API_URL ?? process.env.REACT_APP_API_URL ?? `http://${window.location.hostname}:8001/api`,
connectorBuilderApiUrl: process.env.REACT_APP_CONNECTOR_BUILDER_API_URL ?? `http://${window.location.hostname}:8003`,
version: window.AIRBYTE_VERSION ?? "dev",
integrationUrl: process.env.REACT_APP_INTEGRATION_DOCS_URLS ?? "/docs",
oauthRedirectUrl: `${window.location.protocol}//${window.location.host}`,
cloudApiUrl: window.CLOUD_API_URL ?? process.env.REACT_APP_CLOUD_API_URL,
cloudPublicApiUrl: process.env.REACT_APP_CLOUD_PUBLIC_API_URL,
firebase: {
apiKey: window.FIREBASE_API_KEY ?? process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: window.FIREBASE_AUTH_DOMAIN ?? process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
authEmulatorHost: window.FIREBASE_AUTH_EMULATOR_HOST ?? process.env.REACT_APP_FIREBASE_AUTH_EMULATOR_HOST,
},
intercom: {
appId: process.env.REACT_APP_INTERCOM_APP_ID,
},
launchDarkly: window.LAUNCHDARKLY_KEY ?? process.env.REACT_APP_LAUNCHDARKLY_KEY,
datadog: {
applicationId: window.REACT_APP_DATADOG_APPLICATION_ID ?? process.env.REACT_APP_DATADOG_APPLICATION_ID,
clientToken: window.REACT_APP_DATADOG_CLIENT_TOKEN ?? process.env.REACT_APP_DATADOG_CLIENT_TOKEN,
site: window.REACT_APP_DATADOG_SITE ?? process.env.REACT_APP_DATADOG_SITE,
service: window.REACT_APP_DATADOG_SERVICE ?? process.env.REACT_APP_DATADOG_SERVICE,
},
sentryDsn: window.REACT_APP_SENTRY_DSN ?? process.env.REACT_APP_SENTRY_DSN,
webappTag: window.REACT_APP_WEBAPP_TAG ?? process.env.REACT_APP_WEBAPP_TAG ?? "dev",
};

export class MissingConfigError extends Error {
constructor(message: string) {
super(message);
this.name = "MissingConfigError";
}
}
58 changes: 0 additions & 58 deletions airbyte-webapp/src/config/configProviders.test.ts

This file was deleted.

45 changes: 0 additions & 45 deletions airbyte-webapp/src/config/configProviders.ts

This file was deleted.

13 changes: 0 additions & 13 deletions airbyte-webapp/src/config/defaultConfig.ts

This file was deleted.

3 changes: 1 addition & 2 deletions airbyte-webapp/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./defaultConfig";
export * from "./configProviders";
export * from "./config";
export * from "./ConfigServiceProvider";
export * from "./types";
55 changes: 34 additions & 21 deletions airbyte-webapp/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,50 @@ declare global {
API_URL?: string;
CONNECTOR_BUILDER_API_URL?: string;
CLOUD?: string;
REACT_APP_DATADOG_APPLICATION_ID: string;
REACT_APP_DATADOG_CLIENT_TOKEN: string;
REACT_APP_DATADOG_SITE: string;
REACT_APP_DATADOG_SERVICE: string;
REACT_APP_SENTRY_DSN?: string;
REACT_APP_DATADOG_APPLICATION_ID?: string;
REACT_APP_DATADOG_CLIENT_TOKEN?: string;
REACT_APP_DATADOG_SITE?: string;
REACT_APP_DATADOG_SERVICE?: string;
REACT_APP_WEBAPP_TAG?: string;
REACT_APP_INTERCOM_APP_ID?: string;
REACT_APP_INTEGRATION_DOCS_URLS?: string;
SEGMENT_TOKEN?: string;
LAUNCHDARKLY_KEY?: string;
// Cloud specific properties
FIREBASE_API_KEY?: string;
FIREBASE_AUTH_DOMAIN?: string;
FIREBASE_AUTH_EMULATOR_HOST?: string;
CLOUD_API_URL?: string;
CLOUD_PUBLIC_API_URL?: string;
REACT_APP_SENTRY_DSN?: string;
}
}

export interface Config {
segment: { token: string; enabled: boolean };
export interface AirbyteWebappConfig {
segment: { token?: string; enabled: boolean };
apiUrl: string;
connectorBuilderApiUrl: string;
oauthRedirectUrl: string;
healthCheckInterval: number;
version?: string;
version: string;
integrationUrl: string;
oauthRedirectUrl: string;
cloudApiUrl?: string;
cloudPublicApiUrl?: string;
firebase: {
apiKey?: string;
authDomain?: string;
authEmulatorHost?: string;
};
intercom: {
appId?: string;
};
launchDarkly?: string;
datadog: {
applicationId?: string;
clientToken?: string;
site?: string;
service?: string;
tag?: string;
};
webappTag?: string;
sentryDsn?: string;
}

export type DeepPartial<T> = {
[P in keyof T]+?: DeepPartial<T[P]>;
};

export type ProviderAsync<T> = () => Promise<T>;
export type Provider<T> = () => T;

export type ValueProvider<T> = Array<ProviderAsync<DeepPartial<T>>>;

export type ConfigProvider<T extends Config = Config> = ProviderAsync<DeepPartial<T>>;
5 changes: 2 additions & 3 deletions airbyte-webapp/src/core/request/apiOverride.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { CommonRequestError } from "./CommonRequestError";
import { RequestMiddleware } from "./RequestMiddleware";
import { VersionError } from "./VersionError";
import { Config } from "../../config";
import { AirbyteWebappConfig } from "../../config";

export interface ApiOverrideRequestOptions {
config: Pick<Config, "apiUrl">;
config: Pick<AirbyteWebappConfig, "apiUrl">;
middlewares: RequestMiddleware[];
signal?: RequestInit["signal"];
}
Expand All @@ -15,7 +15,6 @@ function getRequestBody<U>(data: U) {
if (nonJsonObject) {
// The app tries to stringify blobs which results in broken functionality.
// There may be some edge cases where we pass in an empty object.
// @ts-expect-error There may be a better way to do this, but for now it solves the problem.
return data as BodyInit;
}
return stringifiedData;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect, useState } from "react";
import { useIntl } from "react-intl";

import { useConfig } from "config";
import { HealthService } from "core/health/HealthService";
import { useGetService } from "core/servicesProvider";
import { useNotificationService } from "hooks/services/Notification/NotificationService";
Expand All @@ -11,11 +10,11 @@ import { Notification } from "../Notification";

const HEALTH_NOTIFICATION_ID = "health.error";
const HEALTHCHECK_MAX_COUNT = 3;
const HEALTHCHECK_INTERVAL = 20000;

function useApiHealthPoll(): void {
const [count, setCount] = useState(0);
const { formatMessage } = useIntl();
const { healthCheckInterval } = useConfig();
const healthService = useGetService<HealthService>("HealthService");
const { registerNotification, unregisterNotificationById } = useNotificationService();

Expand All @@ -40,10 +39,10 @@ function useApiHealthPoll(): void {
registerNotification(errorNotification);
}
}
}, healthCheckInterval);
}, HEALTHCHECK_INTERVAL);

return () => clearInterval(interval);
}, [count, healthCheckInterval, formatMessage, unregisterNotificationById, registerNotification, healthService]);
}, [count, formatMessage, unregisterNotificationById, registerNotification, healthService]);
}

export { useApiHealthPoll };
Loading

0 comments on commit 502e1b0

Please sign in to comment.