Skip to content

Commit

Permalink
🪟 🎉 Add frontend redirect for Cloud API token management (#19488)
Browse files Browse the repository at this point in the history
* add open api spec & orval config

* add page & data fetching logic for speakeasy

* typo

* update error copy

* Missed comma in merge conflict.

* commit orval-generated api methods

* create hook for calling cloud api endpoint

* remove orval config

* Address feedback

* Change CTA text

* Move to speakeasy folder

* Use new spearate cloudPublicApiUrl

* Change i18n message

* Extract types

* Revert accidental change

Co-authored-by: Bryce Groff <bryce@airbyte.io>
Co-authored-by: Bryce Groff <bgroff@hawaii.edu>
Co-authored-by: Tim Roes <tim@airbyte.io>
  • Loading branch information
4 people authored Dec 2, 2022
1 parent 5fc2a6b commit 09154f3
Show file tree
Hide file tree
Showing 14 changed files with 131 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { AppActionCodes } from "./actionCodes";

const appMonitoringContext = createContext<AppMonitoringServiceProviderValue | null>(null);

export type TrackActionFn = (actionCode: AppActionCodes, context?: Record<string, unknown>) => void;
export type TrackErrorFn = (error: Error, context?: Record<string, unknown>) => void;

/**
* The AppMonitoringService exposes methods for tracking actions and errors from the webapp.
* These methods are particularly useful for tracking when unexpected or edge-case conditions
Expand All @@ -14,11 +17,11 @@ interface AppMonitoringServiceProviderValue {
/**
* Log a custom action in datadog. Useful for tracking edge cases or unexpected application states.
*/
trackAction: (actionCode: AppActionCodes, context?: Record<string, unknown>) => void;
trackAction: TrackActionFn;
/**
* Log a custom error in datadog. Useful for tracking edge case errors while handling them in the UI.
*/
trackError: (error: Error, context?: Record<string, unknown>) => void;
trackError: TrackErrorFn;
}

export const useAppMonitoringService = (): AppMonitoringServiceProviderValue => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export type { TrackActionFn, TrackErrorFn } from "./AppMonitoringService";

export { AppMonitoringServiceProvider, useAppMonitoringService } from "./AppMonitoringService";
export { AppActionCodes } from "./actionCodes";
5 changes: 4 additions & 1 deletion airbyte-webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@
"frequency.hours": "{value, plural, one {# hour} other {# hours}}",

"ui.goBack": "Go back",
"ui.goToHome": "Go to homepage",
"ui.input.showPassword": "Show password",
"ui.input.hidePassword": "Hide password",
"ui.keyValuePair": "{key}: {value}",
Expand Down Expand Up @@ -592,5 +593,7 @@
"connectorBuilder.unknownError": "An unknown error has occurred",
"connectorBuilder.testConnector": "TEST YOUR CONNECTOR",
"connectorBuilder.couldNotDetectStreams": "Could not detect streams in the YAML editor:",
"connectorBuilder.ensureProperYaml": "In order to test a stream, ensure that the YAML is structured as described in <a>the docs</a>."
"connectorBuilder.ensureProperYaml": "In order to test a stream, ensure that the YAML is structured as described in <a>the docs</a>.",

"cloudApi.loginCallbackUrlError": "There was an error connecting to the developer portal. Please try again."
}
2 changes: 2 additions & 0 deletions airbyte-webapp/src/packages/cloud/cloudRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { DestinationItemPage } from "pages/destination/DestinationItemPage";
import { DestinationOverviewPage } from "pages/destination/DestinationOverviewPage";
import { DestinationSettingsPage } from "pages/destination/DestinationSettingsPage";
import SourcesPage from "pages/SourcesPage";
import { SpeakeasyRedirectPage } from "pages/SpeakeasyRedirectPage";
import { useCurrentWorkspace, WorkspaceServiceProvider } from "services/workspaces/WorkspacesService";
import { setSegmentAnonymousId, useGetSegmentAnonymousId } from "utils/crossDomainUtils";
import { storeUtmFromQuery } from "utils/utmStorage";
Expand Down Expand Up @@ -109,6 +110,7 @@ const MainViewRoutes = () => {

return (
<Routes>
<Route path={RoutePaths.SpeakeasyRedirect} element={<SpeakeasyRedirectPage />} />
{[CloudRoutes.Login, CloudRoutes.Signup, CloudRoutes.FirebaseAction].map((r) => (
<Route key={r} path={`${r}/*`} element={query.from ? <Navigate to={query.from} replace /> : <DefaultView />} />
))}
Expand Down
22 changes: 22 additions & 0 deletions airbyte-webapp/src/packages/cloud/lib/domain/speakeasy/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { apiOverride } from "core/request/apiOverride";

export interface RedirectUrlResponse {
redirectUrl?: string;
}

// eslint-disable-next-line
type SecondParameter<T extends (...args: any) => any> = T extends (config: any, args: infer P) => any ? P : never;

/**
* Return a JSON datastructure that contains the URL that should be redirected to in the redirectUrl field.
* @summary Get the Speakeasy Callback URL
*/
export const getSpeakeasyCallbackUrl = (options?: SecondParameter<typeof apiOverride>, signal?: AbortSignal) => {
return apiOverride<RedirectUrlResponse>({ url: `/speakeasy_callback_url`, method: "get", signal }, options);
};

type AwaitedInput<T> = PromiseLike<T> | T;

type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;

export type GetSpeakeasyCallbackUrlResult = NonNullable<Awaited<ReturnType<typeof getSpeakeasyCallbackUrl>>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./api";
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const cloudWindowConfigProvider: ConfigProvider<CloudConfig> = async () => {
const cloudEnvConfigProvider: ConfigProvider<CloudConfig> = async () => {
return {
cloudApiUrl: process.env.REACT_APP_CLOUD_API_URL,
cloudPublicApiUrl: process.env.REACT_APP_CLOUD_PUBLIC_API_URL,
firebase: {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
Expand Down
1 change: 1 addition & 0 deletions airbyte-webapp/src/packages/cloud/services/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function useConfig(): CloudConfig {

const cloudConfigExtensionDefault: CloudConfigExtension = {
cloudApiUrl: "",
cloudPublicApiUrl: "/cloud_api",
firebase: {
apiKey: "",
authDomain: "",
Expand Down
2 changes: 2 additions & 0 deletions airbyte-webapp/src/packages/cloud/services/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ declare global {
FIREBASE_AUTH_DOMAIN?: string;
FIREBASE_AUTH_EMULATOR_HOST?: string;
CLOUD_API_URL?: string;
CLOUD_PUBLIC_API_URL?: string;
}
}

export interface CloudConfigExtension {
cloudApiUrl: string;
cloudPublicApiUrl: string;
firebase: {
apiKey: string;
authDomain: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useSpeakeasyRedirect";
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { getSpeakeasyCallbackUrl } from "packages/cloud/lib/domain/speakeasy";
import { useSuspenseQuery } from "services/connector/useSuspenseQuery";
import { useDefaultRequestMiddlewares } from "services/useDefaultRequestMiddlewares";

import { useConfig } from "../config";

const SPEAKEASY_QUERY_KEY = "speakeasy-redirect";

export const useSpeakeasyRedirect = () => {
const { cloudPublicApiUrl } = useConfig();
const config = { apiUrl: cloudPublicApiUrl };
const middlewares = useDefaultRequestMiddlewares();
const requestOptions = { config, middlewares };

return useSuspenseQuery([SPEAKEASY_QUERY_KEY], () => getSpeakeasyCallbackUrl(requestOptions));
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Suspense, useEffect } from "react";
import React from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";

import { LoadingPage } from "components/LoadingPage";

import { useAppMonitoringService, TrackErrorFn } from "hooks/services/AppMonitoringService";
import { useSpeakeasyRedirect } from "packages/cloud/services/speakeasy";
import { RoutePaths } from "pages/routePaths";
import { ErrorOccurredView } from "views/common/ErrorOccurredView";

export const SpeakeasyRedirectPage = () => {
const { trackError } = useAppMonitoringService();

return (
<SpeakeasyErrorBoundary trackError={trackError}>
<Suspense fallback={<LoadingPage />}>
<SpeakeasyLoginRedirect />
</Suspense>
</SpeakeasyErrorBoundary>
);
};

const SpeakeasyLoginRedirect = () => {
const { redirectUrl } = useSpeakeasyRedirect();

useEffect(() => {
if (redirectUrl) {
window.location.replace(redirectUrl);
}
}, [redirectUrl]);

return redirectUrl ? <LoadingPage /> : <CloudApiErrorView />;
};

const CloudApiErrorView = () => {
const navigate = useNavigate();
return (
<ErrorOccurredView
message={<FormattedMessage id="cloudApi.loginCallbackUrlError" />}
ctaButtonText={<FormattedMessage id="ui.goToHome" />}
onCtaButtonClick={() => {
navigate(RoutePaths.Root);
}}
/>
);
};

interface SpeakeasyErrorBoundaryProps {
trackError: TrackErrorFn;
}

export class SpeakeasyErrorBoundary extends React.Component<React.PropsWithChildren<SpeakeasyErrorBoundaryProps>> {
state = { error: null };

static getDerivedStateFromError(error: Error) {
return { error };
}

componentDidCatch(error: Error): void {
this.props.trackError(error);
}

render() {
if (this.state.error) {
return <CloudApiErrorView />;
}
return this.props.children;
}
}
1 change: 1 addition & 0 deletions airbyte-webapp/src/pages/SpeakeasyRedirectPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SpeakeasyRedirectPage } from "./SpeakeasyRedirectPage";
2 changes: 2 additions & 0 deletions airbyte-webapp/src/pages/routePaths.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export enum RoutePaths {
AuthFlow = "/auth_flow",
Root = "/",

SpeakeasyRedirect = "speakeasy-redirect",

Workspaces = "workspaces",
Preferences = "preferences",
Connections = "connections",
Expand Down

0 comments on commit 09154f3

Please sign in to comment.