From 09154f3351fadcaa799edb29169d09e3daed8f0a Mon Sep 17 00:00:00 2001 From: Joey Marshment-Howell Date: Fri, 2 Dec 2022 21:50:47 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=9F=20=F0=9F=8E=89=20=20Add=20frontend?= =?UTF-8?q?=20redirect=20for=20Cloud=20API=20token=20management=20(#19488)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: Bryce Groff Co-authored-by: Tim Roes --- .../AppMonitoringService.tsx | 7 +- .../services/AppMonitoringService/index.ts | 2 + airbyte-webapp/src/locales/en.json | 5 +- .../src/packages/cloud/cloudRoutes.tsx | 2 + .../cloud/lib/domain/speakeasy/api.ts | 22 ++++++ .../cloud/lib/domain/speakeasy/index.ts | 1 + .../cloud/services/config/configProviders.ts | 1 + .../packages/cloud/services/config/index.ts | 1 + .../packages/cloud/services/config/types.ts | 2 + .../cloud/services/speakeasy/index.ts | 1 + .../speakeasy/useSpeakeasyRedirect.ts | 16 +++++ .../SpeakeasyRedirectPage.tsx | 71 +++++++++++++++++++ .../src/pages/SpeakeasyRedirectPage/index.tsx | 1 + airbyte-webapp/src/pages/routePaths.tsx | 2 + 14 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 airbyte-webapp/src/packages/cloud/lib/domain/speakeasy/api.ts create mode 100644 airbyte-webapp/src/packages/cloud/lib/domain/speakeasy/index.ts create mode 100644 airbyte-webapp/src/packages/cloud/services/speakeasy/index.ts create mode 100644 airbyte-webapp/src/packages/cloud/services/speakeasy/useSpeakeasyRedirect.ts create mode 100644 airbyte-webapp/src/pages/SpeakeasyRedirectPage/SpeakeasyRedirectPage.tsx create mode 100644 airbyte-webapp/src/pages/SpeakeasyRedirectPage/index.tsx diff --git a/airbyte-webapp/src/hooks/services/AppMonitoringService/AppMonitoringService.tsx b/airbyte-webapp/src/hooks/services/AppMonitoringService/AppMonitoringService.tsx index 8f44132b9700..3256be214cf5 100644 --- a/airbyte-webapp/src/hooks/services/AppMonitoringService/AppMonitoringService.tsx +++ b/airbyte-webapp/src/hooks/services/AppMonitoringService/AppMonitoringService.tsx @@ -5,6 +5,9 @@ import { AppActionCodes } from "./actionCodes"; const appMonitoringContext = createContext(null); +export type TrackActionFn = (actionCode: AppActionCodes, context?: Record) => void; +export type TrackErrorFn = (error: Error, context?: Record) => 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 @@ -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) => 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) => void; + trackError: TrackErrorFn; } export const useAppMonitoringService = (): AppMonitoringServiceProviderValue => { diff --git a/airbyte-webapp/src/hooks/services/AppMonitoringService/index.ts b/airbyte-webapp/src/hooks/services/AppMonitoringService/index.ts index ba69c7120194..0614e990773f 100644 --- a/airbyte-webapp/src/hooks/services/AppMonitoringService/index.ts +++ b/airbyte-webapp/src/hooks/services/AppMonitoringService/index.ts @@ -1,2 +1,4 @@ +export type { TrackActionFn, TrackErrorFn } from "./AppMonitoringService"; + export { AppMonitoringServiceProvider, useAppMonitoringService } from "./AppMonitoringService"; export { AppActionCodes } from "./actionCodes"; diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 9bb3413b2321..1c517233789e 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -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}", @@ -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 the docs." + "connectorBuilder.ensureProperYaml": "In order to test a stream, ensure that the YAML is structured as described in the docs.", + + "cloudApi.loginCallbackUrlError": "There was an error connecting to the developer portal. Please try again." } diff --git a/airbyte-webapp/src/packages/cloud/cloudRoutes.tsx b/airbyte-webapp/src/packages/cloud/cloudRoutes.tsx index 7222075a061c..db4fea04ab25 100644 --- a/airbyte-webapp/src/packages/cloud/cloudRoutes.tsx +++ b/airbyte-webapp/src/packages/cloud/cloudRoutes.tsx @@ -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"; @@ -109,6 +110,7 @@ const MainViewRoutes = () => { return ( + } /> {[CloudRoutes.Login, CloudRoutes.Signup, CloudRoutes.FirebaseAction].map((r) => ( : } /> ))} diff --git a/airbyte-webapp/src/packages/cloud/lib/domain/speakeasy/api.ts b/airbyte-webapp/src/packages/cloud/lib/domain/speakeasy/api.ts new file mode 100644 index 000000000000..5e8c0af4a35c --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/lib/domain/speakeasy/api.ts @@ -0,0 +1,22 @@ +import { apiOverride } from "core/request/apiOverride"; + +export interface RedirectUrlResponse { + redirectUrl?: string; +} + +// eslint-disable-next-line +type SecondParameter 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, signal?: AbortSignal) => { + return apiOverride({ url: `/speakeasy_callback_url`, method: "get", signal }, options); +}; + +type AwaitedInput = PromiseLike | T; + +type Awaited = O extends AwaitedInput ? T : never; + +export type GetSpeakeasyCallbackUrlResult = NonNullable>>; diff --git a/airbyte-webapp/src/packages/cloud/lib/domain/speakeasy/index.ts b/airbyte-webapp/src/packages/cloud/lib/domain/speakeasy/index.ts new file mode 100644 index 000000000000..d158c5764011 --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/lib/domain/speakeasy/index.ts @@ -0,0 +1 @@ +export * from "./api"; diff --git a/airbyte-webapp/src/packages/cloud/services/config/configProviders.ts b/airbyte-webapp/src/packages/cloud/services/config/configProviders.ts index 3072d364cd5b..1a08b895fc96 100644 --- a/airbyte-webapp/src/packages/cloud/services/config/configProviders.ts +++ b/airbyte-webapp/src/packages/cloud/services/config/configProviders.ts @@ -39,6 +39,7 @@ const cloudWindowConfigProvider: ConfigProvider = async () => { const cloudEnvConfigProvider: ConfigProvider = 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, diff --git a/airbyte-webapp/src/packages/cloud/services/config/index.ts b/airbyte-webapp/src/packages/cloud/services/config/index.ts index 812c0d2249dd..1063c3d68a71 100644 --- a/airbyte-webapp/src/packages/cloud/services/config/index.ts +++ b/airbyte-webapp/src/packages/cloud/services/config/index.ts @@ -8,6 +8,7 @@ export function useConfig(): CloudConfig { const cloudConfigExtensionDefault: CloudConfigExtension = { cloudApiUrl: "", + cloudPublicApiUrl: "/cloud_api", firebase: { apiKey: "", authDomain: "", diff --git a/airbyte-webapp/src/packages/cloud/services/config/types.ts b/airbyte-webapp/src/packages/cloud/services/config/types.ts index 4d3ab389efb9..990b50f5b7f5 100644 --- a/airbyte-webapp/src/packages/cloud/services/config/types.ts +++ b/airbyte-webapp/src/packages/cloud/services/config/types.ts @@ -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; diff --git a/airbyte-webapp/src/packages/cloud/services/speakeasy/index.ts b/airbyte-webapp/src/packages/cloud/services/speakeasy/index.ts new file mode 100644 index 000000000000..2ef818c00b78 --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/services/speakeasy/index.ts @@ -0,0 +1 @@ +export * from "./useSpeakeasyRedirect"; diff --git a/airbyte-webapp/src/packages/cloud/services/speakeasy/useSpeakeasyRedirect.ts b/airbyte-webapp/src/packages/cloud/services/speakeasy/useSpeakeasyRedirect.ts new file mode 100644 index 000000000000..d38a7a71fd6a --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/services/speakeasy/useSpeakeasyRedirect.ts @@ -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)); +}; diff --git a/airbyte-webapp/src/pages/SpeakeasyRedirectPage/SpeakeasyRedirectPage.tsx b/airbyte-webapp/src/pages/SpeakeasyRedirectPage/SpeakeasyRedirectPage.tsx new file mode 100644 index 000000000000..e51748afb731 --- /dev/null +++ b/airbyte-webapp/src/pages/SpeakeasyRedirectPage/SpeakeasyRedirectPage.tsx @@ -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 ( + + }> + + + + ); +}; + +const SpeakeasyLoginRedirect = () => { + const { redirectUrl } = useSpeakeasyRedirect(); + + useEffect(() => { + if (redirectUrl) { + window.location.replace(redirectUrl); + } + }, [redirectUrl]); + + return redirectUrl ? : ; +}; + +const CloudApiErrorView = () => { + const navigate = useNavigate(); + return ( + } + ctaButtonText={} + onCtaButtonClick={() => { + navigate(RoutePaths.Root); + }} + /> + ); +}; + +interface SpeakeasyErrorBoundaryProps { + trackError: TrackErrorFn; +} + +export class SpeakeasyErrorBoundary extends React.Component> { + state = { error: null }; + + static getDerivedStateFromError(error: Error) { + return { error }; + } + + componentDidCatch(error: Error): void { + this.props.trackError(error); + } + + render() { + if (this.state.error) { + return ; + } + return this.props.children; + } +} diff --git a/airbyte-webapp/src/pages/SpeakeasyRedirectPage/index.tsx b/airbyte-webapp/src/pages/SpeakeasyRedirectPage/index.tsx new file mode 100644 index 000000000000..11a607c7d6d7 --- /dev/null +++ b/airbyte-webapp/src/pages/SpeakeasyRedirectPage/index.tsx @@ -0,0 +1 @@ +export { SpeakeasyRedirectPage } from "./SpeakeasyRedirectPage"; diff --git a/airbyte-webapp/src/pages/routePaths.tsx b/airbyte-webapp/src/pages/routePaths.tsx index c6f3ef1b8918..32bcb9954021 100644 --- a/airbyte-webapp/src/pages/routePaths.tsx +++ b/airbyte-webapp/src/pages/routePaths.tsx @@ -2,6 +2,8 @@ export enum RoutePaths { AuthFlow = "/auth_flow", Root = "/", + SpeakeasyRedirect = "speakeasy-redirect", + Workspaces = "workspaces", Preferences = "preferences", Connections = "connections",