diff --git a/packages/ui/app/src/playground/PlaygroundAuthorizationForm.tsx b/packages/ui/app/src/playground/PlaygroundAuthorizationForm.tsx deleted file mode 100644 index 4db35034f0..0000000000 --- a/packages/ui/app/src/playground/PlaygroundAuthorizationForm.tsx +++ /dev/null @@ -1,594 +0,0 @@ -import type { EndpointContext } from "@fern-api/fdr-sdk/api-definition"; -import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; -import { visitDiscriminatedUnion } from "@fern-api/ui-core-utils"; -import { - FernButton, - FernCard, - FernCollapse, - FernDropdown, - FernInput, - FernSegmentedControl, - FernTooltip, - FernTooltipProvider, -} from "@fern-ui/components"; -import { useBooleanState } from "@fern-ui/react-commons"; -import { HelpCircle, Key, User } from "iconoir-react"; -import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"; -import { useSearchParams } from "next/navigation"; -import { FC, ReactElement, SetStateAction, useCallback, useEffect, useState } from "react"; -import urlJoin from "url-join"; -import { useMemoOne } from "use-memo-one"; -import { - PLAYGROUND_AUTH_STATE_ATOM, - PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM, - PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM, - PLAYGROUND_AUTH_STATE_HEADER_ATOM, - PLAYGROUND_AUTH_STATE_OAUTH_ATOM, - usePlaygroundEndpointFormState, -} from "../atoms"; -import { useApiRoute } from "../hooks/useApiRoute"; -import { useStandardProxyEnvironment } from "../hooks/useStandardProxyEnvironment"; -import { Callout } from "../mdx/components/callout"; -import { useApiKeyInjectionConfig } from "../services/useApiKeyInjectionConfig"; -import { PasswordInputGroup } from "./PasswordInputGroup"; -import { PlaygroundEndpointForm } from "./endpoint/PlaygroundEndpointForm"; -import { useOAuthEndpointContext } from "./hooks/useOauthEndpointContext"; -import { PlaygroundAuthState } from "./types"; -import { oAuthClientCredentialReferencedEndpointLoginFlow } from "./utils/oauth"; -import { usePlaygroundBaseUrl } from "./utils/select-environment"; - -interface PlaygroundAuthorizationFormProps { - auth: APIV1Read.ApiAuth; - closeContainer: () => void; - disabled: boolean; -} - -function BearerAuthForm({ bearerAuth, disabled }: { bearerAuth: APIV1Read.BearerAuth; disabled?: boolean }) { - const [value, setValue] = useAtom(PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM); - const handleChange = useCallback((newValue: string) => setValue({ token: newValue }), [setValue]); - - return ( -
  • - - -
    - -
    -
  • - ); -} - -function BasicAuthForm({ basicAuth, disabled }: { basicAuth: APIV1Read.BasicAuth; disabled?: boolean }) { - const [value, setValue] = useAtom(PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM); - const handleChangeUsername = useCallback( - (newValue: string) => setValue((prev) => ({ ...prev, username: newValue })), - [setValue], - ); - const handleChangePassword = useCallback( - (newValue: string) => setValue((prev) => ({ ...prev, password: newValue })), - [setValue], - ); - return ( - <> -
  • - -
    - } - rightElement={{"string"}} - disabled={disabled} - /> -
    -
  • - -
  • - - -
    - -
    -
  • - - ); -} - -function HeaderAuthForm({ header, disabled }: { header: APIV1Read.HeaderAuth; disabled?: boolean }) { - const [value, setValue] = useAtom( - useMemoOne( - () => - atom( - (get) => get(PLAYGROUND_AUTH_STATE_HEADER_ATOM).headers[header.headerWireValue], - (_get, set, change: SetStateAction) => { - set(PLAYGROUND_AUTH_STATE_HEADER_ATOM, ({ headers }) => ({ - headers: { - ...headers, - [header.headerWireValue]: - typeof change === "function" - ? change(headers[header.headerWireValue] ?? "") - : change, - }, - })); - }, - ), - [header.headerWireValue], - ), - ); - - return ( -
  • - -
    - -
    -
  • - ); -} - -function FoundOAuthReferencedEndpointForm({ - context, - oAuthClientCredentialsReferencedEndpoint, - closeContainer, - disabled, -}: { - /** - * this must be the OAuth endpoint. - */ - context: EndpointContext; - oAuthClientCredentialsReferencedEndpoint: APIV1Read.OAuthClientCredentialsReferencedEndpoint; - closeContainer: () => void; - disabled?: boolean; -}) { - const [value, setValue] = useAtom(PLAYGROUND_AUTH_STATE_OAUTH_ATOM); - const proxyEnvironment = useStandardProxyEnvironment(); - const [formState, setFormState] = usePlaygroundEndpointFormState(context); - const [baseUrl] = usePlaygroundBaseUrl(context.endpoint); - - const [displayFailedLogin, setDisplayFailedLogin] = useState(false); - - /** - * TODO: turn this into a loadable (suspense) - */ - const oAuthClientCredentialLogin = async () => { - setValue((prev) => ({ ...prev, isLoggingIn: true })); - await oAuthClientCredentialReferencedEndpointLoginFlow({ - formState, - endpoint: context.endpoint, - proxyEnvironment, - oAuthClientCredentialsReferencedEndpoint, - baseUrl, - setValue, - closeContainer, - setDisplayFailedLogin, - }); - setValue((prev) => ({ ...prev, isLoggingIn: false })); - }; - - const authenticationOptions: FernDropdown.Option[] = [ - { type: "value", value: "credentials", label: "Credentials", icon: }, - { type: "value", value: "token", label: "Bearer Token", icon: }, - ]; - - return value.isLoggingIn ? ( -
  • Loading...
  • - ) : ( - <> -
  • - { - if (value != null && value.length > 0) { - setValue((prev) => ({ ...prev, selectedInputMethod: value as "credentials" | "token" })); - } - }} - value={value.selectedInputMethod} - disabled={disabled} - /> -
  • - - {value.selectedInputMethod === "credentials" ? ( - <> -
  • - - -
  • - {displayFailedLogin && ( - Failed to login with the provided credentials - )} - {value.isLoggedIn && ( -
  • - -
    - -
    -
    - -
  • - )} - {value.isLoggedIn && value.accessToken !== value.loggedInStartingToken && ( - - The bearer token is no longer valid. Please refresh it by clicking the button below - - )} - - ) : ( - <> -
  • - - - - setValue((prev) => ({ ...prev, userSuppliedAccessToken: newValue })) - } - value={value.userSuppliedAccessToken} - autoComplete="off" - data-1p-ignore="true" - disabled={disabled} - /> -
  • - - )} -
  • - {value.selectedInputMethod === "credentials" && ( - - )} -
  • - - ); -} - -function OAuthReferencedEndpointForm({ - referencedEndpoint, - oAuthClientCredentialsReferencedEndpoint, - closeContainer, - disabled, -}: { - referencedEndpoint: APIV1Read.OAuthClientCredentials.ReferencedEndpoint; - oAuthClientCredentialsReferencedEndpoint: APIV1Read.OAuthClientCredentialsReferencedEndpoint; - closeContainer: () => void; - disabled?: boolean; -}) { - const { context, isLoading } = useOAuthEndpointContext(referencedEndpoint); - - if (context == null) { - if (!isLoading) { - // eslint-disable-next-line no-console - console.error("Could not find OAuth endpoint for referenced endpoint", referencedEndpoint); - } - return ; - } - - return ( - - ); -} - -function OAuthForm({ - oAuth, - closeContainer, - disabled, -}: { - oAuth: APIV1Read.ApiAuth.OAuth; - closeContainer: () => void; - disabled?: boolean; -}) { - return visitDiscriminatedUnion(oAuth.value, "type")._visit({ - clientCredentials: (clientCredentials) => { - return visitDiscriminatedUnion(clientCredentials.value, "type")._visit({ - referencedEndpoint: (referencedEndpoint) => { - return ( - - ); - }, - _other: () => null, - }); - }, - _other: () => null, - }); -} - -export const PlaygroundAuthorizationForm: FC = ({ - auth, - closeContainer, - disabled, -}) => { - return ( -
      - {visitDiscriminatedUnion(auth, "type")._visit({ - bearerAuth: (bearerAuth) => , - basicAuth: (basicAuth) => , - header: (header) => , - oAuth: (oAuth) => , - _other: () => null, - })} -
    - ); -}; - -interface PlaygroundAuthorizationFormCardProps { - auth: APIV1Read.ApiAuth; - disabled: boolean; -} - -export function PlaygroundAuthorizationFormCard({ - auth, - disabled, -}: PlaygroundAuthorizationFormCardProps): ReactElement | null { - const authState = useAtomValue(PLAYGROUND_AUTH_STATE_ATOM); - const setBearerAuth = useSetAtom(PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM); - const oAuth = useAtomValue(PLAYGROUND_AUTH_STATE_OAUTH_ATOM); - const isOpen = useBooleanState(false); - const apiKeyInjection = useApiKeyInjectionConfig(); - const apiKey = apiKeyInjection.enabled && apiKeyInjection.authenticated ? apiKeyInjection.access_token : null; - const searchParams = useSearchParams(); - const error = searchParams.get("error"); - const errorDescription = searchParams.get("error_description"); - - const handleResetBearerAuth = useCallback(() => { - setBearerAuth({ token: apiKey ?? "" }); - }, [apiKey, setBearerAuth]); - - const logoutApiRoute = useApiRoute("/api/fern-docs/auth/logout"); - - const redirectOrOpenAuthForm = () => { - if (apiKeyInjection.enabled && !apiKeyInjection.authenticated) { - const url = new URL(apiKeyInjection.authorizationUrl); - const state = new URL(window.location.href); - if (state.searchParams.has("error")) { - state.searchParams.delete("error"); - } - if (state.searchParams.has("error_description")) { - state.searchParams.delete("error_description"); - } - url.searchParams.set(apiKeyInjection.returnToQueryParam, state.toString()); - window.location.replace(url); - } else { - isOpen.toggleValue(); - } - }; - - const authButtonCopy = apiKeyInjection.enabled - ? "Login to send a real request" - : `Authenticate with your ${ - auth.type === "oAuth" - ? oAuth.selectedInputMethod === "credentials" - ? "credentials" - : "bearer token" - : "API key" - } to send a real request`; - - // TODO change this to on-login - useEffect(() => { - if (apiKey != null) { - setBearerAuth({ token: apiKey }); - } - }, [apiKey, setBearerAuth]); - - return ( -
    - {apiKeyInjection.enabled && apiKey == null && ( - <> - - {error && {errorDescription ?? error}} - -
    Login to send a real request
    -
    - } - onClick={redirectOrOpenAuthForm} - /> - } - text="Provide token manually" - onClick={() => isOpen.toggleValue()} - /> -
    -
    - - )} - {apiKeyInjection.enabled && apiKey != null && ( - <> - - } - active={true} - /> -
    - -
    - { -
    - {apiKey !== authState?.bearerAuth?.token && apiKey && ( - } - onClick={handleResetBearerAuth} - size="normal" - variant="outlined" - /> - )} - {apiKeyInjection && ( - { - if (!apiKeyInjection.authenticated) { - return; - } - const url = new URL(urlJoin(window.location.origin, logoutApiRoute)); - const returnTo = new URL(window.location.href); - url.searchParams.set( - apiKeyInjection.returnToQueryParam, - returnTo.toString(), - ); - fetch(url) - .then(() => { - window.location.reload(); - }) - .catch((error) => { - // eslint-disable-next-line no-console - console.error(error); - }); - }} - size="normal" - variant="outlined" - /> - )} -
    - } -
    - - )} - {!apiKeyInjection.enabled && (isAuthed(auth, authState) || apiKey != null) && ( - } - rightIcon={ - - Authenticated - - } - onClick={isOpen.toggleValue} - active={isOpen.value} - /> - )} - {!apiKeyInjection.enabled && !(isAuthed(auth, authState) || apiKey != null) && ( - } - rightIcon={ - - Not Authenticated - - } - onClick={redirectOrOpenAuthForm} - active={isOpen.value} - /> - )} - - -
    -
    - - -
    - {auth.type !== "oAuth" && ( - - )} - {apiKey != null && ( - - )} -
    -
    -
    -
    -
    - ); -} - -function isEmpty(str: string | undefined): boolean { - return str == null || str.trim().length === 0; -} - -function isAuthed(auth: APIV1Read.ApiAuth, authState: PlaygroundAuthState): boolean { - return visitDiscriminatedUnion(auth)._visit({ - bearerAuth: () => !isEmpty(authState.bearerAuth?.token.trim()), - basicAuth: () => - !isEmpty(authState.basicAuth?.username.trim()) && !isEmpty(authState.basicAuth?.password.trim()), - header: (header) => !isEmpty(authState.header?.headers[header.headerWireValue]?.trim()), - oAuth: () => { - const authToken = - authState.oauth?.selectedInputMethod === "credentials" - ? authState.oauth?.accessToken - : authState.oauth?.userSuppliedAccessToken; - return authToken ? !isEmpty(authToken.trim()) : false; - }, - _other: () => false, - }); -} diff --git a/packages/ui/app/src/playground/PlaygroundContent.tsx b/packages/ui/app/src/playground/PlaygroundContent.tsx index 8d88692264..a430b8e928 100644 --- a/packages/ui/app/src/playground/PlaygroundContent.tsx +++ b/packages/ui/app/src/playground/PlaygroundContent.tsx @@ -2,11 +2,9 @@ import type * as FernNavigation from "@fern-api/fdr-sdk/navigation"; import { ArrowLeft } from "iconoir-react"; import { ReactElement } from "react"; import { usePlaygroundNode } from "../atoms"; -import { PlaygroundWebSocket } from "./PlaygroundWebSocket"; -import { PlaygroundEndpoint } from "./endpoint/PlaygroundEndpoint"; -import { PlaygroundEndpointSkeleton } from "./endpoint/PlaygroundEndpointSkeleton"; -import { useEndpointContext } from "./hooks/useEndpointContext"; -import { useWebSocketContext } from "./hooks/useWebSocketContext"; +import { PlaygroundEndpoint, PlaygroundEndpointSkeleton } from "./endpoint"; +import { useEndpointContext, useWebSocketContext } from "./hooks"; +import { PlaygroundWebSocket } from "./websocket"; const PlaygroundContentForEndpoint = ({ node }: { node: FernNavigation.EndpointNode }) => { const { context, isLoading } = useEndpointContext(node); diff --git a/packages/ui/app/src/playground/PlaygroundDrawer.tsx b/packages/ui/app/src/playground/PlaygroundDrawer.tsx index 7738c0a593..d1d75a7ddc 100644 --- a/packages/ui/app/src/playground/PlaygroundDrawer.tsx +++ b/packages/ui/app/src/playground/PlaygroundDrawer.tsx @@ -8,20 +8,23 @@ import { useAtomValue, useSetAtom } from "jotai"; import { useAtomCallback } from "jotai/utils"; import { ReactElement, memo, useEffect } from "react"; import { useCallbackOne } from "use-memo-one"; -import { HEADER_HEIGHT_ATOM, useAtomEffect } from "../atoms"; import { + HEADER_HEIGHT_ATOM, + IS_MOBILE_SCREEN_ATOM, MAX_PLAYGROUND_HEIGHT_ATOM, + MOBILE_SIDEBAR_ENABLED_ATOM, PLAYGROUND_NODE_ID, + VIEWPORT_HEIGHT_ATOM, + useAtomEffect, useIsPlaygroundOpen, usePlaygroundFormStateAtom, usePlaygroundNode, useTogglePlayground, -} from "../atoms/playground"; -import { IS_MOBILE_SCREEN_ATOM, MOBILE_SIDEBAR_ENABLED_ATOM, VIEWPORT_HEIGHT_ATOM } from "../atoms/viewport"; +} from "../atoms"; import { FernErrorBoundary } from "../components/FernErrorBoundary"; import { PlaygroundContent } from "./PlaygroundContent"; import { HorizontalSplitPane } from "./VerticalSplitPane"; -import { PlaygroundEndpointSelectorContent } from "./endpoint/PlaygroundEndpointSelectorContent"; +import { PlaygroundEndpointSelectorContent } from "./endpoint"; import { useResizeY } from "./useSplitPlane"; import { PLAYGROUND_API_GROUPS_ATOM } from "./utils/flatten-apis"; diff --git a/packages/ui/app/src/playground/auth/PlaygroundAuthorizationForm.tsx b/packages/ui/app/src/playground/auth/PlaygroundAuthorizationForm.tsx new file mode 100644 index 0000000000..78f89bde21 --- /dev/null +++ b/packages/ui/app/src/playground/auth/PlaygroundAuthorizationForm.tsx @@ -0,0 +1,33 @@ +import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; +import { visitDiscriminatedUnion } from "@fern-api/ui-core-utils"; +import { FC, ReactElement } from "react"; +import { PlaygroundBasicAuthForm } from "./PlaygroundBasicAuthForm"; +import { PlaygroundBearerAuthForm } from "./PlaygroundBearerAuthForm"; +import { PlaygroundHeaderAuthForm } from "./PlaygroundHeaderAuthForm"; +import { PlaygroundOAuthForm } from "./PlaygroundOAuthForm"; + +interface PlaygroundAuthorizationFormProps { + auth: APIV1Read.ApiAuth; + closeContainer: () => void; + disabled: boolean; +} + +export const PlaygroundAuthorizationForm: FC = ({ + auth, + closeContainer, + disabled, +}) => { + return ( +
      + {visitDiscriminatedUnion(auth, "type")._visit({ + bearerAuth: (bearerAuth) => , + basicAuth: (basicAuth) => , + header: (header) => , + oAuth: (oAuth) => ( + + ), + _other: () => false, + })} +
    + ); +}; diff --git a/packages/ui/app/src/playground/auth/PlaygroundAuthorizationFormCard.tsx b/packages/ui/app/src/playground/auth/PlaygroundAuthorizationFormCard.tsx new file mode 100644 index 0000000000..6093a21b4e --- /dev/null +++ b/packages/ui/app/src/playground/auth/PlaygroundAuthorizationFormCard.tsx @@ -0,0 +1,72 @@ +import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; +import { FernButton, FernCollapse } from "@fern-ui/components"; +import { useBooleanState } from "@fern-ui/react-commons"; +import { useSetAtom } from "jotai/react"; +import { ReactElement } from "react"; +import { PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM } from "../../atoms"; +import { useApiKeyInjectionConfig } from "../../services/useApiKeyInjectionConfig"; +import { PlaygroundAuthorizationForm } from "./PlaygroundAuthorizationForm"; +import { PlaygroundCardTriggerApiKeyInjected } from "./PlaygroundCardTriggerApiKeyInjected"; +import { PlaygroundCardTriggerManual } from "./PlaygroundCardTriggerManual"; + +interface PlaygroundAuthorizationFormCardProps { + auth: APIV1Read.ApiAuth; + disabled: boolean; +} +export function PlaygroundAuthorizationFormCard({ + auth, + disabled, +}: PlaygroundAuthorizationFormCardProps): ReactElement | null { + const setBearerAuth = useSetAtom(PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM); + const isOpen = useBooleanState(false); + const apiKeyInjection = useApiKeyInjectionConfig(); + const apiKey = apiKeyInjection.enabled && apiKeyInjection.authenticated ? apiKeyInjection.access_token : null; + + const handleResetBearerAuth = () => { + setBearerAuth({ token: apiKey ?? "" }); + }; + + return ( +
    + {apiKeyInjection.enabled ? ( + + ) : ( + + )} + + +
    +
    + + +
    + {auth.type !== "oAuth" && ( + + )} + {apiKey != null && ( + + )} +
    +
    +
    +
    +
    + ); +} diff --git a/packages/ui/app/src/playground/auth/PlaygroundBasicAuthForm.tsx b/packages/ui/app/src/playground/auth/PlaygroundBasicAuthForm.tsx new file mode 100644 index 0000000000..c1fdbde28b --- /dev/null +++ b/packages/ui/app/src/playground/auth/PlaygroundBasicAuthForm.tsx @@ -0,0 +1,57 @@ +import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; +import { FernInput } from "@fern-ui/components"; +import { User } from "iconoir-react"; +import { useAtom } from "jotai/react"; +import { ReactElement, useCallback } from "react"; +import { PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM } from "../../atoms"; +import { PasswordInputGroup } from "../PasswordInputGroup"; + +export function PlaygroundBasicAuthForm({ + basicAuth, + disabled, +}: { + basicAuth: APIV1Read.BasicAuth; + disabled?: boolean; +}): ReactElement { + const [value, setValue] = useAtom(PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM); + const handleChangeUsername = useCallback( + (newValue: string) => setValue((prev) => ({ ...prev, username: newValue })), + [setValue], + ); + const handleChangePassword = useCallback( + (newValue: string) => setValue((prev) => ({ ...prev, password: newValue })), + [setValue], + ); + return ( + <> +
  • + +
    + } + rightElement={{"string"}} + disabled={disabled} + /> +
    +
  • + +
  • + + +
    + +
    +
  • + + ); +} diff --git a/packages/ui/app/src/playground/auth/PlaygroundBearerAuthForm.tsx b/packages/ui/app/src/playground/auth/PlaygroundBearerAuthForm.tsx new file mode 100644 index 0000000000..f48976a74c --- /dev/null +++ b/packages/ui/app/src/playground/auth/PlaygroundBearerAuthForm.tsx @@ -0,0 +1,34 @@ +import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; +import { useAtom } from "jotai/react"; +import { ReactElement, useCallback } from "react"; +import { PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM } from "../../atoms"; +import { PasswordInputGroup } from "../PasswordInputGroup"; + +export function PlaygroundBearerAuthForm({ + bearerAuth, + disabled, +}: { + bearerAuth: APIV1Read.BearerAuth; + disabled?: boolean; +}): ReactElement { + const [value, setValue] = useAtom(PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM); + const handleChange = useCallback((newValue: string) => setValue({ token: newValue }), [setValue]); + + return ( +
  • + + +
    + +
    +
  • + ); +} diff --git a/packages/ui/app/src/playground/auth/PlaygroundCardTriggerApiKeyInjected.tsx b/packages/ui/app/src/playground/auth/PlaygroundCardTriggerApiKeyInjected.tsx new file mode 100644 index 0000000000..fbb4c332b0 --- /dev/null +++ b/packages/ui/app/src/playground/auth/PlaygroundCardTriggerApiKeyInjected.tsx @@ -0,0 +1,145 @@ +import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; +import { FernButton, FernCard } from "@fern-ui/components"; +import { APIKeyInjectionConfigEnabled } from "@fern-ui/fern-docs-auth"; +import { Key, User } from "iconoir-react"; +import { useAtomValue, useSetAtom } from "jotai"; +import { useSearchParams } from "next/navigation"; +import { ReactElement, useEffect } from "react"; +import urlJoin from "url-join"; +import { PLAYGROUND_AUTH_STATE_ATOM, PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM } from "../../atoms"; +import { useApiRoute } from "../../hooks/useApiRoute"; +import { Callout } from "../../mdx/components/callout"; +import { PlaygroundAuthorizationForm } from "./PlaygroundAuthorizationForm"; + +interface PlaygroundCardTriggerApiKeyInjectedProps { + auth: APIV1Read.ApiAuth; + config: APIKeyInjectionConfigEnabled; + disabled: boolean; + toggleOpen: () => void; + onClose: () => void; +} + +export function PlaygroundCardTriggerApiKeyInjected({ + auth, + config, + disabled, + toggleOpen, + onClose, +}: PlaygroundCardTriggerApiKeyInjectedProps): ReactElement | false { + const searchParams = useSearchParams(); + const error = searchParams.get("error"); + const errorDescription = searchParams.get("error_description"); + const authState = useAtomValue(PLAYGROUND_AUTH_STATE_ATOM); + const logoutApiRoute = useApiRoute("/api/fern-docs/auth/logout"); + + const apiKey = config.authenticated ? config.access_token : null; + const setBearerAuth = useSetAtom(PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM); + + // TODO change this to on-login + useEffect(() => { + if (apiKey != null) { + setBearerAuth({ token: apiKey }); + } + }, [apiKey, setBearerAuth]); + + const handleResetBearerAuth = () => { + setBearerAuth({ token: apiKey ?? "" }); + }; + + const redirectOrOpenAuthForm = () => { + if (!config.authenticated) { + const url = new URL(config.authorizationUrl); + const state = new URL(window.location.href); + if (state.searchParams.has("error")) { + state.searchParams.delete("error"); + } + if (state.searchParams.has("error_description")) { + state.searchParams.delete("error_description"); + } + url.searchParams.set(config.returnToQueryParam, state.toString()); + window.location.replace(url); + } else { + toggleOpen(); + } + }; + + if (apiKey != null) { + return ( + + } + active={true} + /> +
    + +
    + { +
    + {apiKey !== authState?.bearerAuth?.token && apiKey && ( + } + onClick={handleResetBearerAuth} + size="normal" + variant="outlined" + /> + )} + { + if (!config.authenticated) { + return; + } + const url = new URL(urlJoin(window.location.origin, logoutApiRoute)); + const returnTo = new URL(window.location.href); + url.searchParams.set(config.returnToQueryParam, returnTo.toString()); + fetch(url) + .then(() => { + window.location.reload(); + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error(error); + }); + }} + size="normal" + variant="outlined" + /> +
    + } +
    + ); + } + + return ( + + {error && {errorDescription ?? error}} + +
    Login to send a real request
    +
    + } + onClick={redirectOrOpenAuthForm} + /> + } + text="Provide token manually" + onClick={toggleOpen} + /> +
    +
    + ); +} diff --git a/packages/ui/app/src/playground/auth/PlaygroundCardTriggerManual.tsx b/packages/ui/app/src/playground/auth/PlaygroundCardTriggerManual.tsx new file mode 100644 index 0000000000..e2ad97d6c4 --- /dev/null +++ b/packages/ui/app/src/playground/auth/PlaygroundCardTriggerManual.tsx @@ -0,0 +1,87 @@ +import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; +import { visitDiscriminatedUnion } from "@fern-api/ui-core-utils"; +import { FernButton } from "@fern-ui/components"; +import { Key } from "iconoir-react"; +import { useAtomValue } from "jotai"; +import { ReactElement } from "react"; +import { PLAYGROUND_AUTH_STATE_ATOM } from "../../atoms"; +import { PlaygroundAuthState } from "../types"; + +interface PlaygroundCardTriggerManualProps { + auth: APIV1Read.ApiAuth; + disabled: boolean; + toggleOpen: () => void; + isOpen: boolean; +} + +export function PlaygroundCardTriggerManual({ + auth, + disabled, + toggleOpen, + isOpen, +}: PlaygroundCardTriggerManualProps): ReactElement | false { + const authState = useAtomValue(PLAYGROUND_AUTH_STATE_ATOM); + + const authButtonCopy = "Login to send a real request"; + + if (isAuthed(auth, authState)) { + return ( + } + rightIcon={ + + Authenticated + + } + onClick={toggleOpen} + active={isOpen} + disabled={disabled} + /> + ); + } else { + return ( + } + rightIcon={ + + Not Authenticated + + } + onClick={toggleOpen} + active={isOpen} + disabled={disabled} + /> + ); + } +} + +function isEmpty(str: string | undefined): boolean { + return str == null || str.trim().length === 0; +} + +function isAuthed(auth: APIV1Read.ApiAuth, authState: PlaygroundAuthState): boolean { + return visitDiscriminatedUnion(auth)._visit({ + bearerAuth: () => !isEmpty(authState.bearerAuth?.token.trim()), + basicAuth: () => + !isEmpty(authState.basicAuth?.username.trim()) && !isEmpty(authState.basicAuth?.password.trim()), + header: (header) => !isEmpty(authState.header?.headers[header.headerWireValue]?.trim()), + oAuth: () => { + const authToken = + authState.oauth?.selectedInputMethod === "credentials" + ? authState.oauth?.accessToken + : authState.oauth?.userSuppliedAccessToken; + return authToken ? !isEmpty(authToken.trim()) : false; + }, + _other: () => false, + }); +} diff --git a/packages/ui/app/src/playground/auth/PlaygroundHeaderAuthForm.tsx b/packages/ui/app/src/playground/auth/PlaygroundHeaderAuthForm.tsx new file mode 100644 index 0000000000..bb5f447b3e --- /dev/null +++ b/packages/ui/app/src/playground/auth/PlaygroundHeaderAuthForm.tsx @@ -0,0 +1,53 @@ +import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; +import { atom } from "jotai"; +import { useAtom } from "jotai/react"; +import type { ReactElement, SetStateAction } from "react"; +import { useMemoOne } from "use-memo-one"; +import { PLAYGROUND_AUTH_STATE_HEADER_ATOM } from "../../atoms"; +import { PasswordInputGroup } from "../PasswordInputGroup"; + +export function PlaygroundHeaderAuthForm({ + header, + disabled, +}: { + header: APIV1Read.HeaderAuth; + disabled?: boolean; +}): ReactElement { + const [value, setValue] = useAtom( + useMemoOne( + () => + atom( + (get) => get(PLAYGROUND_AUTH_STATE_HEADER_ATOM).headers[header.headerWireValue], + (_get, set, change: SetStateAction) => { + set(PLAYGROUND_AUTH_STATE_HEADER_ATOM, ({ headers }) => ({ + headers: { + ...headers, + [header.headerWireValue]: + typeof change === "function" + ? change(headers[header.headerWireValue] ?? "") + : change, + }, + })); + }, + ), + [header.headerWireValue], + ), + ); + + return ( +
  • + +
    + +
    +
  • + ); +} diff --git a/packages/ui/app/src/playground/auth/PlaygroundOAuthForm.tsx b/packages/ui/app/src/playground/auth/PlaygroundOAuthForm.tsx new file mode 100644 index 0000000000..eb822aebb4 --- /dev/null +++ b/packages/ui/app/src/playground/auth/PlaygroundOAuthForm.tsx @@ -0,0 +1,207 @@ +import { EndpointContext } from "@fern-api/fdr-sdk/api-definition"; +import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; +import { visitDiscriminatedUnion } from "@fern-api/ui-core-utils"; +import { FernButton, FernDropdown, FernSegmentedControl, FernTooltip, FernTooltipProvider } from "@fern-ui/components"; +import { HelpCircle, Key, User } from "iconoir-react"; +import { useAtom } from "jotai"; +import { ReactElement, useState } from "react"; +import { PLAYGROUND_AUTH_STATE_OAUTH_ATOM, usePlaygroundEndpointFormState } from "../../atoms"; +import { useStandardProxyEnvironment } from "../../hooks/useStandardProxyEnvironment"; +import { Callout } from "../../mdx/components/callout"; +import { PasswordInputGroup } from "../PasswordInputGroup"; +import { PlaygroundEndpointForm } from "../endpoint"; +import { useOAuthEndpointContext } from "../hooks"; +import { oAuthClientCredentialReferencedEndpointLoginFlow } from "../utils/oauth"; +import { usePlaygroundBaseUrl } from "../utils/select-environment"; +import { PlaygroundBearerAuthForm } from "./PlaygroundBearerAuthForm"; + +function FoundOAuthReferencedEndpointForm({ + context, + oAuthClientCredentialsReferencedEndpoint, + closeContainer, + disabled, +}: { + /** + * this must be the OAuth endpoint. + */ + context: EndpointContext; + oAuthClientCredentialsReferencedEndpoint: APIV1Read.OAuthClientCredentialsReferencedEndpoint; + closeContainer: () => void; + disabled?: boolean; +}): ReactElement { + const [value, setValue] = useAtom(PLAYGROUND_AUTH_STATE_OAUTH_ATOM); + const proxyEnvironment = useStandardProxyEnvironment(); + const [formState, setFormState] = usePlaygroundEndpointFormState(context); + const [baseUrl] = usePlaygroundBaseUrl(context.endpoint); + + const [displayFailedLogin, setDisplayFailedLogin] = useState(false); + + /** + * TODO: turn this into a loadable (suspense) + */ + const oAuthClientCredentialLogin = async () => { + setValue((prev) => ({ ...prev, isLoggingIn: true })); + await oAuthClientCredentialReferencedEndpointLoginFlow({ + formState, + endpoint: context.endpoint, + proxyEnvironment, + oAuthClientCredentialsReferencedEndpoint, + baseUrl, + setValue, + closeContainer, + setDisplayFailedLogin, + }); + setValue((prev) => ({ ...prev, isLoggingIn: false })); + }; + + const authenticationOptions: FernDropdown.Option[] = [ + { type: "value", value: "credentials", label: "Credentials", icon: }, + { type: "value", value: "token", label: "Bearer Token", icon: }, + ]; + + return value.isLoggingIn ? ( +
  • Loading...
  • + ) : ( + <> +
  • + { + if (value != null && value.length > 0) { + setValue((prev) => ({ ...prev, selectedInputMethod: value as "credentials" | "token" })); + } + }} + value={value.selectedInputMethod} + disabled={disabled} + /> +
  • + + {value.selectedInputMethod === "credentials" ? ( + <> +
  • + + +
  • + {displayFailedLogin && ( + Failed to login with the provided credentials + )} + {value.isLoggedIn && ( +
  • + +
    + +
    +
    + +
  • + )} + {value.isLoggedIn && value.accessToken !== value.loggedInStartingToken && ( + + The bearer token is no longer valid. Please refresh it by clicking the button below + + )} + + ) : ( + <> +
  • + + + + setValue((prev) => ({ ...prev, userSuppliedAccessToken: newValue })) + } + value={value.userSuppliedAccessToken} + autoComplete="off" + data-1p-ignore="true" + disabled={disabled} + /> +
  • + + )} +
  • + {value.selectedInputMethod === "credentials" && ( + + )} +
  • + + ); +} + +function OAuthReferencedEndpointForm({ + referencedEndpoint, + oAuthClientCredentialsReferencedEndpoint, + closeContainer, + disabled, +}: { + referencedEndpoint: APIV1Read.OAuthClientCredentials.ReferencedEndpoint; + oAuthClientCredentialsReferencedEndpoint: APIV1Read.OAuthClientCredentialsReferencedEndpoint; + closeContainer: () => void; + disabled?: boolean; +}) { + const { context, isLoading } = useOAuthEndpointContext(referencedEndpoint); + + if (context == null) { + if (!isLoading) { + // eslint-disable-next-line no-console + console.error("Could not find OAuth endpoint for referenced endpoint", referencedEndpoint); + } + return ; + } + + return ( + + ); +} + +export function PlaygroundOAuthForm({ + oAuth, + closeContainer, + disabled, +}: { + oAuth: APIV1Read.ApiAuth.OAuth; + closeContainer: () => void; + disabled?: boolean; +}): ReactElement | false { + return visitDiscriminatedUnion(oAuth.value, "type")._visit({ + clientCredentials: (clientCredentials) => { + return visitDiscriminatedUnion(clientCredentials.value, "type")._visit({ + referencedEndpoint: (referencedEndpoint) => ( + + ), + _other: () => false, + }); + }, + _other: () => false, + }); +} diff --git a/packages/ui/app/src/playground/auth/index.ts b/packages/ui/app/src/playground/auth/index.ts new file mode 100644 index 0000000000..03a2e3c123 --- /dev/null +++ b/packages/ui/app/src/playground/auth/index.ts @@ -0,0 +1 @@ +export { PlaygroundAuthorizationFormCard } from "./PlaygroundAuthorizationFormCard"; diff --git a/packages/ui/app/src/playground/endpoint/PlaygroundEndpointContent.tsx b/packages/ui/app/src/playground/endpoint/PlaygroundEndpointContent.tsx index ea1abda6f1..e9ba05112b 100644 --- a/packages/ui/app/src/playground/endpoint/PlaygroundEndpointContent.tsx +++ b/packages/ui/app/src/playground/endpoint/PlaygroundEndpointContent.tsx @@ -1,7 +1,7 @@ import type { EndpointContext } from "@fern-api/fdr-sdk/api-definition"; import { Loadable } from "@fern-ui/loadable"; import { Dispatch, ReactElement, SetStateAction, useDeferredValue } from "react"; -import { PlaygroundAuthorizationFormCard } from "../PlaygroundAuthorizationForm"; +import { PlaygroundAuthorizationFormCard } from "../auth"; import { PlaygroundEndpointRequestFormState } from "../types"; import { PlaygroundResponse } from "../types/playgroundResponse"; import { PlaygroundEndpointContentLayout } from "./PlaygroundEndpointContentLayout"; diff --git a/packages/ui/app/src/playground/endpoint/index.ts b/packages/ui/app/src/playground/endpoint/index.ts new file mode 100644 index 0000000000..2df8e62fa0 --- /dev/null +++ b/packages/ui/app/src/playground/endpoint/index.ts @@ -0,0 +1,4 @@ +export { PlaygroundEndpoint } from "./PlaygroundEndpoint"; +export { PlaygroundEndpointForm } from "./PlaygroundEndpointForm"; +export { PlaygroundEndpointSelectorContent } from "./PlaygroundEndpointSelectorContent"; +export { PlaygroundEndpointSkeleton } from "./PlaygroundEndpointSkeleton"; diff --git a/packages/ui/app/src/playground/hooks/index.ts b/packages/ui/app/src/playground/hooks/index.ts new file mode 100644 index 0000000000..38f532a428 --- /dev/null +++ b/packages/ui/app/src/playground/hooks/index.ts @@ -0,0 +1,5 @@ +export { useEndpointContext } from "./useEndpointContext"; +export { useOAuthEndpointContext } from "./useOAuthEndpointContext"; +export { usePreloadApiLeaf } from "./usePreloadApiLeaf"; +export { useWebSocketContext } from "./useWebSocketContext"; +export { useWebsocketMessages } from "./useWebsocketMessages"; diff --git a/packages/ui/app/src/playground/hooks/useOauthEndpointContext.ts b/packages/ui/app/src/playground/hooks/useOAuthEndpointContext.ts similarity index 100% rename from packages/ui/app/src/playground/hooks/useOauthEndpointContext.ts rename to packages/ui/app/src/playground/hooks/useOAuthEndpointContext.ts diff --git a/packages/ui/app/src/playground/PlaygroundWebSocket.tsx b/packages/ui/app/src/playground/websocket/PlaygroundWebSocket.tsx similarity index 95% rename from packages/ui/app/src/playground/PlaygroundWebSocket.tsx rename to packages/ui/app/src/playground/websocket/PlaygroundWebSocket.tsx index 23c2db3f58..8b50119606 100644 --- a/packages/ui/app/src/playground/PlaygroundWebSocket.tsx +++ b/packages/ui/app/src/playground/websocket/PlaygroundWebSocket.tsx @@ -4,13 +4,13 @@ import { FernTooltipProvider } from "@fern-ui/components"; import { usePrevious } from "@fern-ui/react-commons"; import { Wifi, WifiOff } from "iconoir-react"; import { FC, ReactElement, useCallback, useEffect, useRef, useState } from "react"; -import { PLAYGROUND_AUTH_STATE_ATOM, store, usePlaygroundWebsocketFormState } from "../atoms"; -import { usePlaygroundSettings } from "../hooks/usePlaygroundSettings"; +import { PLAYGROUND_AUTH_STATE_ATOM, store, usePlaygroundWebsocketFormState } from "../../atoms"; +import { usePlaygroundSettings } from "../../hooks/usePlaygroundSettings"; +import { PlaygroundEndpointPath } from "../endpoint/PlaygroundEndpointPath"; +import { useWebsocketMessages } from "../hooks/useWebsocketMessages"; +import { buildAuthHeaders } from "../utils"; +import { usePlaygroundBaseUrl } from "../utils/select-environment"; import { PlaygroundWebSocketContent } from "./PlaygroundWebSocketContent"; -import { PlaygroundEndpointPath } from "./endpoint/PlaygroundEndpointPath"; -import { useWebsocketMessages } from "./hooks/useWebsocketMessages"; -import { buildAuthHeaders } from "./utils"; -import { usePlaygroundBaseUrl } from "./utils/select-environment"; // TODO: decide if this should be an env variable, and if we should move REST proxy to the same (or separate) cloudflare worker const WEBSOCKET_PROXY_URI = "wss://websocket.proxy.ferndocs.com/ws"; diff --git a/packages/ui/app/src/playground/PlaygroundWebSocketContent.tsx b/packages/ui/app/src/playground/websocket/PlaygroundWebSocketContent.tsx similarity index 97% rename from packages/ui/app/src/playground/PlaygroundWebSocketContent.tsx rename to packages/ui/app/src/playground/websocket/PlaygroundWebSocketContent.tsx index ea0de8014e..9f20821d1a 100644 --- a/packages/ui/app/src/playground/PlaygroundWebSocketContent.tsx +++ b/packages/ui/app/src/playground/websocket/PlaygroundWebSocketContent.tsx @@ -1,8 +1,8 @@ import type * as ApiDefinition from "@fern-api/fdr-sdk/api-definition"; import type { WebSocketContext } from "@fern-api/fdr-sdk/api-definition"; import { Dispatch, FC, SetStateAction, useEffect, useRef, useState } from "react"; +import { PlaygroundWebSocketRequestFormState } from "../types"; import { PlaygroundWebSocketSessionForm } from "./PlaygroundWebSocketSessionForm"; -import { PlaygroundWebSocketRequestFormState } from "./types"; interface PlaygroundWebSocketContentProps { context: WebSocketContext; diff --git a/packages/ui/app/src/playground/PlaygroundWebSocketHandshakeForm.tsx b/packages/ui/app/src/playground/websocket/PlaygroundWebSocketHandshakeForm.tsx similarity index 94% rename from packages/ui/app/src/playground/PlaygroundWebSocketHandshakeForm.tsx rename to packages/ui/app/src/playground/websocket/PlaygroundWebSocketHandshakeForm.tsx index 66579a49df..b6bc2c6a32 100644 --- a/packages/ui/app/src/playground/PlaygroundWebSocketHandshakeForm.tsx +++ b/packages/ui/app/src/playground/websocket/PlaygroundWebSocketHandshakeForm.tsx @@ -1,10 +1,10 @@ import type { WebSocketContext } from "@fern-api/fdr-sdk/api-definition"; import { FernCard } from "@fern-ui/components"; import { Dispatch, FC, SetStateAction, useCallback } from "react"; -import { Callout } from "../mdx/components/callout"; -import { PlaygroundAuthorizationFormCard } from "./PlaygroundAuthorizationForm"; -import { PlaygroundObjectPropertiesForm } from "./form/PlaygroundObjectPropertyForm"; -import { PlaygroundWebSocketRequestFormState } from "./types"; +import { Callout } from "../../mdx/components/callout"; +import { PlaygroundAuthorizationFormCard } from "../auth"; +import { PlaygroundObjectPropertiesForm } from "../form/PlaygroundObjectPropertyForm"; +import { PlaygroundWebSocketRequestFormState } from "../types"; interface PlaygroundWebSocketHandshakeFormProps { context: WebSocketContext; diff --git a/packages/ui/app/src/playground/PlaygroundWebSocketSessionForm.tsx b/packages/ui/app/src/playground/websocket/PlaygroundWebSocketSessionForm.tsx similarity index 94% rename from packages/ui/app/src/playground/PlaygroundWebSocketSessionForm.tsx rename to packages/ui/app/src/playground/websocket/PlaygroundWebSocketSessionForm.tsx index df079fb943..c5231dbb81 100644 --- a/packages/ui/app/src/playground/PlaygroundWebSocketSessionForm.tsx +++ b/packages/ui/app/src/playground/websocket/PlaygroundWebSocketSessionForm.tsx @@ -4,12 +4,12 @@ import titleCase from "@fern-api/ui-core-utils/titleCase"; import { FernButton, FernCard, FernScrollArea } from "@fern-ui/components"; import cn from "clsx"; import { Dispatch, FC, SetStateAction, useCallback } from "react"; -import { WebSocketMessagesVirtualized } from "../api-reference/web-socket/WebSocketMessagesVirtualized"; +import { WebSocketMessagesVirtualized } from "../../api-reference/web-socket/WebSocketMessagesVirtualized"; +import { HorizontalSplitPane } from "../VerticalSplitPane"; +import { PlaygroundTypeReferenceForm } from "../form/PlaygroundTypeReferenceForm"; +import { useWebsocketMessages } from "../hooks/useWebsocketMessages"; +import { PlaygroundWebSocketRequestFormState } from "../types"; import { PlaygroundWebSocketHandshakeForm } from "./PlaygroundWebSocketHandshakeForm"; -import { HorizontalSplitPane } from "./VerticalSplitPane"; -import { PlaygroundTypeReferenceForm } from "./form/PlaygroundTypeReferenceForm"; -import { useWebsocketMessages } from "./hooks/useWebsocketMessages"; -import { PlaygroundWebSocketRequestFormState } from "./types"; interface PlaygroundWebSocketSessionFormProps { context: WebSocketContext; diff --git a/packages/ui/app/src/playground/websocket/index.ts b/packages/ui/app/src/playground/websocket/index.ts new file mode 100644 index 0000000000..e8a7d3f9b2 --- /dev/null +++ b/packages/ui/app/src/playground/websocket/index.ts @@ -0,0 +1 @@ +export { PlaygroundWebSocket } from "./PlaygroundWebSocket"; diff --git a/packages/ui/fern-docs-auth/src/injection.ts b/packages/ui/fern-docs-auth/src/injection.ts index ed3fab8db5..675422f65d 100644 --- a/packages/ui/fern-docs-auth/src/injection.ts +++ b/packages/ui/fern-docs-auth/src/injection.ts @@ -21,13 +21,18 @@ export const APIKeyInjectionConfigAuthorizedSchema = z.object({ returnToQueryParam: z.string(), }); -export const APIKeyInjectionConfigSchema = z.union([ - APIKeyInjectionConfigDisabledSchema, +export const APIKeyInjectionConfigEnabledSchema = z.union([ APIKeyInjectionConfigUnauthorizedSchema, APIKeyInjectionConfigAuthorizedSchema, ]); +export const APIKeyInjectionConfigSchema = z.union([ + APIKeyInjectionConfigDisabledSchema, + APIKeyInjectionConfigEnabledSchema, +]); + export type APIKeyInjectionConfigDisabled = z.infer; +export type APIKeyInjectionConfigEnabled = z.infer; export type APIKeyInjectionConfigUnauthorized = z.infer; export type APIKeyInjectionConfigAuthorized = z.infer; export type APIKeyInjectionConfig = z.infer;