Skip to content

Commit

Permalink
feat: render pdf and a download button in API Playground (#791)
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity authored May 3, 2024
1 parent abd5aef commit a1f59a7
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 26 deletions.
7 changes: 6 additions & 1 deletion packages/ui/app/src/api-playground/PlaygroundDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,12 @@ export const PlaygroundDrawer: FC<PlaygroundDrawerProps> = ({ apis }) => {
)}
<div className="flex h-full flex-col rounded-lg">
<div>{layoutBreakpoint === "mobile" ? mobileHeader : desktopHeader}</div>
<FernErrorBoundary component="PlaygroundDrawer" className="flex h-full items-center justify-center">
<FernErrorBoundary
component="PlaygroundDrawer"
className="flex h-full items-center justify-center"
showError={true}
reset={resetWithoutExample}
>
{selectionState?.type === "endpoint" && matchedEndpoint != null ? (
<PlaygroundEndpoint
endpoint={matchedEndpoint}
Expand Down
51 changes: 46 additions & 5 deletions packages/ui/app/src/api-playground/PlaygroundEndpointContent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Loadable, visitLoadable } from "@fern-ui/loadable";
import { PaperPlaneIcon } from "@radix-ui/react-icons";
import { DownloadIcon, PaperPlaneIcon } from "@radix-ui/react-icons";
import cn from "clsx";
import { useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
Expand All @@ -10,7 +10,9 @@ import { FernButton, FernButtonGroup } from "../components/FernButton";
import { FernCard } from "../components/FernCard";
import { FernErrorTag } from "../components/FernErrorBoundary";
import { FernTabs } from "../components/FernTabs";
import { FernTooltip, FernTooltipProvider } from "../components/FernTooltip";
import { useFeatureFlags } from "../contexts/FeatureFlagContext";
import { useDocsContext } from "../contexts/docs-context/useDocsContext";
import { useLayoutBreakpoint } from "../contexts/layout-breakpoint/useLayoutBreakpoint";
import { ResolvedEndpointDefinition, ResolvedTypeDefinition } from "../resolver/types";
import { CopyToClipboardButton } from "../syntax-highlighting/CopyToClipboardButton";
Expand All @@ -21,7 +23,7 @@ import { PlaygroundRequestPreview } from "./PlaygroundRequestPreview";
import { PlaygroundResponsePreview } from "./PlaygroundResponsePreview";
import { PlaygroundSendRequestButton } from "./PlaygroundSendRequestButton";
import { HorizontalSplitPane, VerticalSplitPane } from "./VerticalSplitPane";
import { PlaygroundEndpointRequestFormState } from "./types";
import { PlaygroundEndpointRequestFormState, ProxyResponse } from "./types";
import { PlaygroundResponse } from "./types/playgroundResponse";
import { stringifyCurl, stringifyFetch, stringifyPythonRequests } from "./utils";

Expand All @@ -48,6 +50,7 @@ export const PlaygroundEndpointContent: FC<PlaygroundEndpointContentProps> = ({
sendRequest,
types,
}) => {
const { domain } = useDocsContext();
const { isSnippetTemplatesEnabled } = useFeatureFlags();
const [requestType, setRequestType] = useAtom(requestTypeAtom);

Expand Down Expand Up @@ -205,7 +208,21 @@ export const PlaygroundEndpointContent: FC<PlaygroundEndpointContentProps> = ({
loading: () => <div />,
loaded: (response) =>
response.type === "file" ? (
<div />
<FernTooltipProvider>
<FernTooltip content="Download file">
<FernButton
icon={<DownloadIcon />}
size="small"
variant="minimal"
onClick={() => {
const a = document.createElement("a");
a.href = response.response.body;
a.download = createFilename(response.response, response.contentType);
a.click();
}}
/>
</FernTooltip>
</FernTooltipProvider>
) : (
<CopyToClipboardButton
content={() =>
Expand Down Expand Up @@ -237,17 +254,25 @@ export const PlaygroundEndpointContent: FC<PlaygroundEndpointContentProps> = ({
loaded: (response) =>
response.type !== "file" ? (
<PlaygroundResponsePreview response={response} />
) : response.contentType.startsWith("audio/") ? (
) : response.contentType.startsWith("audio/") ||
(domain.includes("ircamamplify") && response.contentType === "binary/octet-stream") ? (
<FernAudioPlayer
src={response.response.body}
title={"Untitled"}
className="flex h-full items-center justify-center p-4"
/>
) : response.contentType.includes("application/pdf") ? (
<iframe
src={response.response.body}
className="size-full"
title="PDF preview"
allowFullScreen
/>
) : (
<FernErrorTag
component="PlaygroundEndpointContent"
error={`File preview not supported for ${response.contentType}`}
className="flex h-full items-center justify-center"
showError
/>
),
failed: (e) => (
Expand Down Expand Up @@ -322,3 +347,19 @@ export const PlaygroundEndpointContent: FC<PlaygroundEndpointContentProps> = ({
</div>
);
};

function createFilename(body: ProxyResponse.SerializableFileBody, contentType: string): string {
const headers = new Headers(body.headers);
const contentDisposition = headers.get("Content-Disposition");

if (contentDisposition != null) {
const filename = contentDisposition.split("filename=")[1];
if (filename != null) {
return filename;
}
}

// TODO: use a more deterministic way to generate filenames
const extension = contentType.split("/")[1];
return `${crypto.randomUUID()}.${extension}`;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Cross1Icon, FileIcon } from "@radix-ui/react-icons";
import { useEventCallback } from "@fern-ui/react-commons";
import { Cross1Icon, FileIcon, FilePlusIcon } from "@radix-ui/react-icons";
import cn from "clsx";
import { uniqBy } from "lodash-es";
import numeral from "numeral";
import { ChangeEventHandler, DragEventHandler, memo, useCallback, useRef, useState } from "react";
import { ChangeEvent, DragEventHandler, memo, useCallback, useRef, useState } from "react";
import { FernButton, FernButtonGroup } from "../../components/FernButton";
import { FernCard } from "../../components/FernCard";
import { WithLabelInternal } from "../WithLabel";
Expand Down Expand Up @@ -33,22 +35,34 @@ export const PlaygroundFileUploadForm = memo<PlaygroundFileUploadFormProps>(
setDrag(false);
};

const handleChangeFiles = (files: FileList | null | undefined) => {
const filesArray = files != null ? Array.from(files) : [];
if (type === "fileArray") {
// append files
onValueChange(uniqueFiles([...(value ?? []), ...filesArray]));
return;
} else {
// replace files
onValueChange(filesArray.length > 0 ? filesArray : undefined);
}
};

const fileDrop: DragEventHandler<HTMLElement> = (e) => {
e.preventDefault();
setDrag(false);

const files = e.dataTransfer.files;
onValueChange(Array.from(files));
handleChangeFiles(files);
};
const handleRemove = useCallback(() => {
onValueChange(undefined);
}, [onValueChange]);
const handleChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
(e) => {
onValueChange(e.target.files != null ? Array.from(e.target.files) : undefined);
},
[onValueChange],
);

const handleChange = useEventCallback((e: ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
handleChangeFiles(files);
});

const ref = useRef<HTMLInputElement>(null);
return (
<WithLabelInternal
Expand All @@ -60,7 +74,14 @@ export const PlaygroundFileUploadForm = memo<PlaygroundFileUploadFormProps>(
availability={undefined}
description={undefined}
>
<input ref={ref} type="file" id={id} onChange={handleChange} className="hidden" />
<input
ref={ref}
type="file"
id={id}
onChange={handleChange}
className="hidden"
multiple={type === "fileArray"}
/>
<FernCard
className={cn("w-full rounded-lg", {
elevated: drag,
Expand All @@ -82,15 +103,15 @@ export const PlaygroundFileUploadForm = memo<PlaygroundFileUploadFormProps>(
/>
</div>
) : (
<div className="divide-default divide-y px-4 py-2">
<div className="divide-default divide-y">
{value.map((file) => (
<div key={file.name} className="flex justify-between">
<div className="flex items-center gap-2">
<div key={file.name} className="flex justify-between py-2 px-4">
<div className="flex items-center gap-2 shrink min-w-0">
<div>
<FileIcon />
</div>
<span className="inline-flex items-baseline gap-2">
<span className="text-sm">{file.name}</span>
<span className="inline-flex items-baseline gap-2 shrink min-w-0">
<span className="text-sm truncate">{file.name}</span>
<span className="t-muted text-xs">
({numeral(file.size).format("0.0b")})
</span>
Expand Down Expand Up @@ -121,9 +142,10 @@ export const PlaygroundFileUploadForm = memo<PlaygroundFileUploadFormProps>(
</div>
))}
{type === "fileArray" && (
<div className="flex justify-end px-4 py-2">
<div className="flex justify-end p-4">
<FernButton
onClick={() => ref.current?.click()}
icon={<FilePlusIcon />}
text="Add more files"
rounded
variant="outlined"
Expand All @@ -140,3 +162,7 @@ export const PlaygroundFileUploadForm = memo<PlaygroundFileUploadFormProps>(
);

PlaygroundFileUploadForm.displayName = "PlaygroundFileUploadForm";

function uniqueFiles(files: File[]): readonly File[] | undefined {
return uniqBy(files, (f) => `${f.webkitRelativePath}/${f.name}/${f.size}`);
}
4 changes: 2 additions & 2 deletions packages/ui/app/src/components/FernAudioPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface FernAudioPlayerProps {
/** The URL of the audio file to play */
src: string;
/** The title of the audio file */
title: string;
title?: string;
author?: string;
/** The author of the audio file */
autoPlay?: boolean;
Expand Down Expand Up @@ -41,7 +41,7 @@ export function FernAudioPlayer(props: FernAudioPlayerProps): ReactElement {
</audio>
<div className="relative flex flex-1 flex-col justify-center gap-1">
<p className="m-0 inline-flex justify-center gap-2 text-center">
<span className="t-accent max-w-[40vw] truncate">{props.title}</span>
{props.title != null && <span className="t-accent max-w-[40vw] truncate">{props.title}</span>}
{props.author && (
<>
<span>&mdash;</span>
Expand Down
34 changes: 32 additions & 2 deletions packages/ui/app/src/components/FernErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { ExclamationTriangleIcon, ReloadIcon } from "@radix-ui/react-icons";
import clsx from "clsx";
import { memoize } from "lodash-es";
import { useRouter } from "next/router";
import React, { PropsWithChildren, ReactElement, useEffect } from "react";
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
import { captureSentryError, captureSentryErrorMessage } from "../analytics/sentry";
import { FernButton } from "./FernButton";

export declare interface FernErrorBoundaryProps {
component?: string; // component displayName where the error occurred
error: unknown;
className?: string;
resetErrorBoundary?: () => void;
reset?: () => void;
refreshOnError?: boolean;
showError?: boolean;
}

export function FernErrorTag({
Expand All @@ -20,12 +23,16 @@ export function FernErrorTag({
className,
errorDescription,
showError,
reset,
resetErrorBoundary,
}: {
component: string; // component displayName where the error occurred
error: unknown;
className?: string;
errorDescription?: string;
showError?: boolean;
reset?: () => void;
resetErrorBoundary?: () => void;
}): ReactElement | null {
useEffect(() => {
// eslint-disable-next-line no-console
Expand All @@ -45,6 +52,18 @@ export function FernErrorTag({
<span className="t-danger inline-flex items-center gap-2 rounded-full bg-tag-danger px-2">
<ExclamationTriangleIcon />
<span>{stringifyError(error)}</span>
{reset != null && (
<FernButton
icon={<ReloadIcon />}
variant="minimal"
rounded
onClick={() => {
reset();
resetErrorBoundary?.();
}}
size="small"
/>
)}
</span>
</div>
);
Expand All @@ -67,7 +86,9 @@ const FernErrorBoundaryInternal: React.FC<FernErrorBoundaryProps> = ({
className,
error,
resetErrorBoundary,
reset,
refreshOnError,
showError,
}) => {
const router = useRouter();

Expand All @@ -91,7 +112,16 @@ const FernErrorBoundaryInternal: React.FC<FernErrorBoundaryProps> = ({
};
}, [resetErrorBoundary, router.events]);

return <FernErrorTag error={error} className={className} component={component ?? "FernErrorBoundary"} />;
return (
<FernErrorTag
error={error}
className={className}
component={component ?? "FernErrorBoundary"}
showError={showError}
reset={reset}
resetErrorBoundary={resetErrorBoundary}
/>
);
};

const getFallbackComponent = memoize(function getFallbackComponent(
Expand Down

0 comments on commit a1f59a7

Please sign in to comment.