diff --git a/packages/ui/app/src/atoms/playground.ts b/packages/ui/app/src/atoms/playground.ts index a9cc421c1d..c38aa8cf9d 100644 --- a/packages/ui/app/src/atoms/playground.ts +++ b/packages/ui/app/src/atoms/playground.ts @@ -32,7 +32,7 @@ import { type ResolvedEndpointDefinition, type ResolvedWebSocketChannel, } from "../resolver/types"; -import { APIS_ATOM, FLATTENED_APIS_ATOM, useFlattenedApi } from "./apis"; +import { FLATTENED_APIS_ATOM, useFlattenedApi } from "./apis"; import { FEATURE_FLAGS_ATOM } from "./flags"; import { useAtomEffect } from "./hooks"; import { HEADER_HEIGHT_ATOM } from "./layout"; @@ -44,10 +44,7 @@ import { IS_MOBILE_SCREEN_ATOM } from "./viewport"; const PLAYGROUND_IS_OPEN_ATOM = atom(false); PLAYGROUND_IS_OPEN_ATOM.debugLabel = "PLAYGROUND_IS_OPEN_ATOM"; -export const HAS_PLAYGROUND_ATOM = atom( - (get) => get(FEATURE_FLAGS_ATOM).isApiPlaygroundEnabled && Object.keys(get(APIS_ATOM)).length > 0, -); -HAS_PLAYGROUND_ATOM.debugLabel = "HAS_PLAYGROUND_ATOM"; +export const IS_PLAYGROUND_ENABLED_ATOM = atom((get) => get(FEATURE_FLAGS_ATOM).isApiPlaygroundEnabled); export const MAX_PLAYGROUND_HEIGHT_ATOM = atom((get) => { const isMobileScreen = get(IS_MOBILE_SCREEN_ATOM); @@ -116,10 +113,6 @@ PLAYGROUND_NODE.debugLabel = "PLAYGROUND_NODE"; export const PREV_PLAYGROUND_NODE_ID = atom(undefined); PREV_PLAYGROUND_NODE_ID.debugLabel = "PREV_PLAYGROUND_NODE_ID"; -export function useHasPlayground(): boolean { - return useAtomValue(HAS_PLAYGROUND_ATOM); -} - export function usePlaygroundNodeId(): FernNavigation.NodeId | undefined { return useAtomValue(PLAYGROUND_NODE_ID); } @@ -360,3 +353,8 @@ export function usePlaygroundWebsocketFormState( ), ]; } + +export const PLAYGROUND_REQUEST_TYPE_ATOM = atomWithStorage<"curl" | "typescript" | "python">( + "api-playground-atom-alpha", + "curl", +); diff --git a/packages/ui/app/src/components/FernLinkButton.tsx b/packages/ui/app/src/components/FernLinkButton.tsx index 7c2bc7d3c9..d766c42d92 100644 --- a/packages/ui/app/src/components/FernLinkButton.tsx +++ b/packages/ui/app/src/components/FernLinkButton.tsx @@ -1,6 +1,6 @@ -import { FernButtonSharedProps, getButtonClassName, renderButtonContent } from "@fern-ui/components"; +import { ButtonContent, FernButtonSharedProps, getButtonClassName } from "@fern-ui/components"; import Link from "next/link"; -import { ComponentProps, PropsWithChildren, forwardRef } from "react"; +import { ComponentProps, PropsWithChildren, createElement, forwardRef } from "react"; import { FernLink } from "./FernLink"; interface FernLinkButtonProps extends ComponentProps, PropsWithChildren {} @@ -46,7 +46,7 @@ export const FernLinkButton = forwardRef : undefined } > - {renderButtonContent(props)} + {createElement(ButtonContent, { ...props, className: "" })} ); }); diff --git a/packages/ui/app/src/css/globals.scss b/packages/ui/app/src/css/globals.scss index 91adbac7d0..2cbabdfb77 100644 --- a/packages/ui/app/src/css/globals.scss +++ b/packages/ui/app/src/css/globals.scss @@ -7,7 +7,7 @@ @import "./components"; @import "../syntax-highlighting/FernSyntaxHighlighter"; @import "../api-reference"; -@import "../playground/PlaygroundEndpoint"; +@import "../playground"; @import "../components"; @import "../mdx/components"; @import "./utilities"; diff --git a/packages/ui/app/src/playground/PlaygroundAuthorizationForm.tsx b/packages/ui/app/src/playground/PlaygroundAuthorizationForm.tsx index b9bfd3a943..cc353c1d19 100644 --- a/packages/ui/app/src/playground/PlaygroundAuthorizationForm.tsx +++ b/packages/ui/app/src/playground/PlaygroundAuthorizationForm.tsx @@ -33,7 +33,7 @@ import { Callout } from "../mdx/components/callout"; import { ResolvedEndpointDefinition, ResolvedTypeDefinition } from "../resolver/types"; import { useApiKeyInjectionConfig } from "../services/useApiKeyInjectionConfig"; import { PasswordInputGroup } from "./PasswordInputGroup"; -import { PlaygroundEndpointForm } from "./PlaygroundEndpointForm"; +import { PlaygroundEndpointForm } from "./endpoint/PlaygroundEndpointForm"; import { PlaygroundAuthState } from "./types"; import { oAuthClientCredentialReferencedEndpointLoginFlow } from "./utils"; diff --git a/packages/ui/app/src/playground/PlaygroundButton.tsx b/packages/ui/app/src/playground/PlaygroundButton.tsx index 4180fa6989..320da32395 100644 --- a/packages/ui/app/src/playground/PlaygroundButton.tsx +++ b/packages/ui/app/src/playground/PlaygroundButton.tsx @@ -3,17 +3,17 @@ import { FernButton, FernTooltip, FernTooltipProvider } from "@fern-ui/component import { PlaySolid } from "iconoir-react"; import { useAtomValue } from "jotai"; import { FC } from "react"; -import { HAS_PLAYGROUND_ATOM, useSetAndOpenPlayground } from "../atoms"; +import { IS_PLAYGROUND_ENABLED_ATOM, useSetAndOpenPlayground } from "../atoms"; import { usePlaygroundSettings } from "../hooks/usePlaygroundSettings"; export const PlaygroundButton: FC<{ state: FernNavigation.NavigationNodeApiLeaf; }> = ({ state }) => { const openPlayground = useSetAndOpenPlayground(); - const hasPlayground = useAtomValue(HAS_PLAYGROUND_ATOM); + const isPlaygroundEnabled = useAtomValue(IS_PLAYGROUND_ENABLED_ATOM); const settings = usePlaygroundSettings(state.id); - if (!hasPlayground) { + if (!isPlaygroundEnabled) { return null; } diff --git a/packages/ui/app/src/playground/PlaygroundContext.tsx b/packages/ui/app/src/playground/PlaygroundContext.tsx index 3b44f4f051..2868fd14f0 100644 --- a/packages/ui/app/src/playground/PlaygroundContext.tsx +++ b/packages/ui/app/src/playground/PlaygroundContext.tsx @@ -3,7 +3,7 @@ import dynamic from "next/dynamic"; import { FC, useEffect } from "react"; import useSWR from "swr"; import { APIS_ATOM, store } from "../atoms"; -import { HAS_PLAYGROUND_ATOM, useInitPlaygroundRouter } from "../atoms/playground"; +import { IS_PLAYGROUND_ENABLED_ATOM, useInitPlaygroundRouter } from "../atoms/playground"; import { useApiRoute } from "../hooks/useApiRoute"; import { ResolvedRootPackage } from "../resolver/types"; @@ -18,7 +18,7 @@ const fetcher = async (url: string) => { export const PlaygroundContextProvider: FC = () => { const key = useApiRoute("/api/fern-docs/resolve-api"); - const { data } = useSWR | null>(key, fetcher, { + const { data, isLoading } = useSWR | null>(key, fetcher, { revalidateOnFocus: false, }); useEffect(() => { @@ -29,6 +29,6 @@ export const PlaygroundContextProvider: FC = () => { useInitPlaygroundRouter(); - const hasPlayground = useAtomValue(HAS_PLAYGROUND_ATOM); - return hasPlayground ? : null; + const isPlaygroundEnabled = useAtomValue(IS_PLAYGROUND_ENABLED_ATOM); + return isPlaygroundEnabled ? : null; }; diff --git a/packages/ui/app/src/playground/PlaygroundDrawer.tsx b/packages/ui/app/src/playground/PlaygroundDrawer.tsx index aa67061d0c..d45c337cdc 100644 --- a/packages/ui/app/src/playground/PlaygroundDrawer.tsx +++ b/packages/ui/app/src/playground/PlaygroundDrawer.tsx @@ -13,7 +13,6 @@ import { HEADER_HEIGHT_ATOM, useAtomEffect, useFlattenedApis, useSidebarNodes } import { MAX_PLAYGROUND_HEIGHT_ATOM, PLAYGROUND_NODE_ID, - useHasPlayground, useIsPlaygroundOpen, usePlaygroundFormStateAtom, usePlaygroundNode, @@ -22,14 +21,18 @@ import { import { IS_MOBILE_SCREEN_ATOM, MOBILE_SIDEBAR_ENABLED_ATOM, VIEWPORT_HEIGHT_ATOM } from "../atoms/viewport"; import { FernErrorBoundary } from "../components/FernErrorBoundary"; import { isEndpoint, isWebSocket, type ResolvedApiEndpointWithPackage } from "../resolver/types"; -import { PlaygroundEndpoint } from "./PlaygroundEndpoint"; -import { PlaygroundEndpointSelectorContent, flattenApiSection } from "./PlaygroundEndpointSelectorContent"; import { PlaygroundWebSocket } from "./PlaygroundWebSocket"; import { HorizontalSplitPane } from "./VerticalSplitPane"; +import { PlaygroundEndpoint } from "./endpoint/PlaygroundEndpoint"; +import { PlaygroundEndpointSelectorContent, flattenApiSection } from "./endpoint/PlaygroundEndpointSelectorContent"; +import { PlaygroundEndpointSkeleton } from "./endpoint/PlaygroundEndpointSkeleton"; import { useResizeY } from "./useSplitPlane"; -export const PlaygroundDrawer = memo((): ReactElement | null => { - const hasPlayground = useHasPlayground(); +interface PlaygroundDrawerProps { + isLoading: boolean; +} + +export const PlaygroundDrawer = memo(({ isLoading }: PlaygroundDrawerProps): ReactElement | null => { const selectionState = usePlaygroundNode(); const apis = useFlattenedApis(); @@ -115,15 +118,13 @@ export const PlaygroundDrawer = memo((): ReactElement | null => { const setFormState = useSetAtom(usePlaygroundFormStateAtom(selectionState?.id ?? FernNavigation.NodeId(""))); - if (!hasPlayground || apiGroups.length === 0) { - return null; - } - const renderContent = () => selectionState?.type === "endpoint" && matchedEndpoint != null ? ( ) : selectionState?.type === "webSocket" && matchedWebSocket != null ? ( + ) : isLoading ? ( + ) : (
diff --git a/packages/ui/app/src/playground/PlaygroundEndpointContent.tsx b/packages/ui/app/src/playground/PlaygroundEndpointContent.tsx deleted file mode 100644 index 0e0544e357..0000000000 --- a/packages/ui/app/src/playground/PlaygroundEndpointContent.tsx +++ /dev/null @@ -1,347 +0,0 @@ -import { - CopyToClipboardButton, - FernAudioPlayer, - FernButton, - FernButtonGroup, - FernCard, - FernTabs, - FernTooltip, - FernTooltipProvider, -} from "@fern-ui/components"; -import { Loadable, visitLoadable } from "@fern-ui/loadable"; -import cn from "clsx"; -import { Download, SendSolid } from "iconoir-react"; -import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { atomWithStorage } from "jotai/utils"; -import { isEmpty, round } from "lodash-es"; -import { Dispatch, FC, SetStateAction, useDeferredValue, useEffect, useRef, useState } from "react"; -import { - IS_MOBILE_SCREEN_ATOM, - PLAYGROUND_AUTH_STATE_ATOM, - PLAYGROUND_AUTH_STATE_OAUTH_ATOM, - store, - useFeatureFlags, -} from "../atoms"; -import { FernErrorTag } from "../components/FernErrorBoundary"; -import { useStandardProxyEnvironment } from "../hooks/useStandardProxyEnvironment"; -import { ResolvedEndpointDefinition, ResolvedTypeDefinition } from "../resolver/types"; -import { PlaygroundAuthorizationFormCard } from "./PlaygroundAuthorizationForm"; -import { PlaygroundEndpointForm } from "./PlaygroundEndpointForm"; -import { PlaygroundEndpointFormButtons } from "./PlaygroundEndpointFormButtons"; -import { PlaygroundRequestPreview } from "./PlaygroundRequestPreview"; -import { PlaygroundResponsePreview } from "./PlaygroundResponsePreview"; -import { PlaygroundSendRequestButton } from "./PlaygroundSendRequestButton"; -import { HorizontalSplitPane, VerticalSplitPane } from "./VerticalSplitPane"; -import { PlaygroundCodeSnippetResolverBuilder } from "./code-snippets/resolver"; -import { PlaygroundEndpointRequestFormState, ProxyResponse } from "./types"; -import { PlaygroundResponse } from "./types/playgroundResponse"; - -interface PlaygroundEndpointContentProps { - endpoint: ResolvedEndpointDefinition; - formState: PlaygroundEndpointRequestFormState; - setFormState: Dispatch>; - resetWithExample: () => void; - resetWithoutExample: () => void; - response: Loadable; - sendRequest: () => void; - types: Record; -} - -const requestTypeAtom = atomWithStorage<"curl" | "typescript" | "python">("api-playground-atom-alpha", "curl"); - -export const PlaygroundEndpointContent: FC = ({ - endpoint, - formState, - setFormState, - resetWithExample, - resetWithoutExample, - response, - sendRequest, - types, -}) => { - const { isBinaryOctetStreamAudioPlayer, isSnippetTemplatesEnabled, isFileForgeHackEnabled } = useFeatureFlags(); - const [requestType, setRequestType] = useAtom(requestTypeAtom); - - const scrollAreaRef = useRef(null); - const [scrollAreaHeight, setScrollAreaHeight] = useState(0); - - const isMobileScreen = useAtomValue(IS_MOBILE_SCREEN_ATOM); - - const setOAuthValue = useSetAtom(PLAYGROUND_AUTH_STATE_OAUTH_ATOM); - const proxyEnvironment = useStandardProxyEnvironment(); - - useEffect(() => { - if (typeof window === "undefined" || scrollAreaRef.current == null) { - return; - } - const resizeObserver = new window.ResizeObserver(([size]) => { - if (size != null) { - setScrollAreaHeight(size.contentRect.height); - } - }); - resizeObserver.observe(scrollAreaRef.current); - return () => { - resizeObserver.disconnect(); - }; - }, []); - - const deferredFormState = useDeferredValue(formState); - - const form = ( -
- {endpoint.auth != null && } - - - - -
- ); - - const requestCard = ( - -
- Request - - - setRequestType("curl")} - size="small" - variant="minimal" - intent={requestType === "curl" ? "primary" : "none"} - active={requestType === "curl"} - > - cURL - - setRequestType("typescript")} - size="small" - variant="minimal" - intent={requestType === "typescript" ? "primary" : "none"} - active={requestType === "typescript"} - > - TypeScript - - setRequestType("python")} - size="small" - variant="minimal" - intent={requestType === "python" ? "primary" : "none"} - active={requestType === "python"} - > - Python - - - - { - const authState = store.get(PLAYGROUND_AUTH_STATE_ATOM); - const resolver = new PlaygroundCodeSnippetResolverBuilder( - endpoint, - isSnippetTemplatesEnabled, - isFileForgeHackEnabled, - ).create(authState, formState, proxyEnvironment, setOAuthValue); - return resolver.resolve(requestType); - }} - className="-mr-2" - /> -
- -
- ); - - const responseCard = ( - -
- Response - - {response.type === "loaded" && ( -
- = 200 && response.value.response.status < 300, - ["bg-method-delete/10 text-method-delete dark:bg-method-delete-dark/10 dark:text-method-delete-dark"]: - response.value.response.status > 300, - })} - > - status: {response.value.response.status} - - - time: {round(response.value.time, 2)}ms - - {response.value.type === "json" && !isEmpty(response.value.size) && ( - - size: {response.value.size}b - - )} -
- )} - - {visitLoadable(response, { - loading: () =>
, - loaded: (response) => - response.type === "file" ? ( - - - } - size="small" - variant="minimal" - onClick={() => { - const a = document.createElement("a"); - a.href = response.response.body; - a.download = createFilename(response.response, response.contentType); - a.click(); - }} - /> - - - ) : ( - - response.type === "json" - ? JSON.stringify(response.response.body, null, 2) - : response.type === "stream" - ? response.response.body - : "" - } - className="-mr-2" - /> - ), - failed: () => ( - - Failed - - ), - })} -
- {visitLoadable(response, { - loading: () => - response.type === "notStartedLoading" ? ( -
- -
- ) : ( -
Loading...
- ), - loaded: (response) => - response.type !== "file" ? ( - - ) : response.contentType.startsWith("audio/") || - (isBinaryOctetStreamAudioPlayer && response.contentType === "binary/octet-stream") ? ( - - ) : response.contentType.includes("application/pdf") ? ( -