Skip to content

Commit

Permalink
feat: implement ErrorExampleSelect (#841)
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity authored May 10, 2024
1 parent abf6adb commit 785cba5
Show file tree
Hide file tree
Showing 13 changed files with 468 additions and 178 deletions.
3 changes: 3 additions & 0 deletions packages/ui/app/src/api-page/endpoints/EndpointContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,10 @@ export const EndpointContent: React.FC<EndpointContent.Props> = ({
requestCurlJson={requestJson}
hoveredRequestPropertyPath={hoveredRequestPropertyPath}
hoveredResponsePropertyPath={hoveredResponsePropertyPath}
showErrors={showErrors}
selectedError={selectedError}
errors={endpoint.errors}
setSelectedError={setSelectedError}
/>
)}
</div>
Expand Down
138 changes: 106 additions & 32 deletions packages/ui/app/src/api-page/endpoints/EndpointContentCodeSnippets.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
"use client";
import { visitDiscriminatedUnion } from "@fern-ui/core-utils";
import { ReactNode, memo, useMemo } from "react";
import { APIV1Read } from "@fern-api/fdr-sdk";
import { EMPTY_OBJECT, visitDiscriminatedUnion } from "@fern-ui/core-utils";
import { ReactNode, memo, useEffect, useMemo, useState } from "react";
import { PlaygroundButton } from "../../api-playground/PlaygroundButton";
import { StatusCodeTag, statusCodeToIntent } from "../../commons/StatusCodeTag";
import { FernButton, FernButtonGroup } from "../../components/FernButton";
import { FernErrorTag } from "../../components/FernErrorBoundary";
import { mergeEndpointSchemaWithExample } from "../../resolver/SchemaWithExample";
import { ResolvedEndpointDefinition, ResolvedError, ResolvedExampleEndpointCall } from "../../resolver/types";
import {
ResolvedEndpointDefinition,
ResolvedError,
ResolvedExampleEndpointCall,
ResolvedExampleError,
} from "../../resolver/types";
import { AudioExample } from "../examples/AudioExample";
import { CodeSnippetExample, JsonCodeSnippetExample } from "../examples/CodeSnippetExample";
import { JsonPropertyPath } from "../examples/JsonPropertyPath";
import { TitledExample } from "../examples/TitledExample";
import type { CodeExample, CodeExampleGroup } from "../examples/code-example";
import { lineNumberOf } from "../examples/utils";
import { getSuccessMessageForStatus } from "../utils/getSuccessMessageForStatus";
import { getMessageForStatus } from "../utils/getMessageForStatus";
import { WebSocketMessages } from "../web-socket/WebSocketMessages";
import { CodeExampleClientDropdown } from "./CodeExampleClientDropdown";
import { EndpointUrlWithOverflow } from "./EndpointUrlWithOverflow";
import { ErrorCodeSnippetExample } from "./ErrorCodeSnippetExample";
import { ErrorExampleSelect } from "./ErrorExampleSelect";

export declare namespace EndpointContentCodeSnippets {
export interface Props {
Expand All @@ -29,7 +37,10 @@ export declare namespace EndpointContentCodeSnippets {
requestCurlJson: unknown;
hoveredRequestPropertyPath: JsonPropertyPath | undefined;
hoveredResponsePropertyPath: JsonPropertyPath | undefined;
showErrors: boolean;
errors: ResolvedError[];
selectedError: ResolvedError | undefined;
setSelectedError: (error: ResolvedError | undefined) => void;
}
}

Expand All @@ -44,10 +55,63 @@ const UnmemoizedEndpointContentCodeSnippets: React.FC<EndpointContentCodeSnippet
requestCurlJson,
hoveredRequestPropertyPath = [],
hoveredResponsePropertyPath = [],
showErrors,
errors,
selectedError,
setSelectedError,
}) => {
const [selectedErrorExample, setSelectedErrorExample] = useState<ResolvedExampleError | undefined>(undefined);

const handleSelectErrorAndExample = (
error: ResolvedError | undefined,
example: ResolvedExampleError | undefined,
) => {
setSelectedError(error);
setSelectedErrorExample(example);
};

// if the selected error is not in the list of errors, reset the selected error
useEffect(() => {
setSelectedErrorExample((prevSelectedErrorExample) => {
if (selectedError == null) {
return undefined;
} else if (
prevSelectedErrorExample != null &&
selectedError.examples.findIndex((e) => e === prevSelectedErrorExample) === -1
) {
return selectedError.examples[0];
}
return prevSelectedErrorExample;
});
}, [selectedError]);

const exampleWithSchema = useMemo(() => mergeEndpointSchemaWithExample(endpoint, example), [endpoint, example]);
const selectedClientGroup = clients.find((client) => client.language === selectedClient.language);

const successTitle =
exampleWithSchema.responseBody != null
? visitDiscriminatedUnion(exampleWithSchema.responseBody, "type")._visit<ReactNode>({
json: (value) => renderResponseTitle(value.statusCode, endpoint.method),
filename: (value) => renderResponseTitle(value.statusCode, endpoint.method),
stream: () => "Streamed Response",
sse: () => "Server-Sent Events",
_other: () => "Response",
})
: "Response";

const errorSelector = showErrors ? (
<ErrorExampleSelect
selectedError={selectedError}
selectedErrorExample={selectedErrorExample}
errors={errors}
setSelectedErrorAndExample={handleSelectErrorAndExample}
>
{successTitle}
</ErrorExampleSelect>
) : (
<span className="text-sm t-muted">{successTitle}</span>
);

return (
<div className="gap-6 grid grid-rows-[repeat(auto-fit,minmax(0,min-content))] grid-rows w-full">
{/* TODO: Replace this with a proper segmented control component */}
Expand Down Expand Up @@ -109,13 +173,23 @@ const UnmemoizedEndpointContentCodeSnippets: React.FC<EndpointContentCodeSnippet
selectedClient.language === "curl" ? lineNumberOf(requestCodeSnippet, "-d '{") : undefined
}
/>
{selectedError != null && <ErrorCodeSnippetExample resolvedError={selectedError} />}
{selectedError != null && (
<JsonCodeSnippetExample
title={errorSelector}
onClick={(e) => {
e.stopPropagation();
}}
hoveredPropertyPath={hoveredResponsePropertyPath}
json={selectedErrorExample?.responseBody ?? EMPTY_OBJECT}
intent={statusCodeToIntent(selectedError.statusCode)}
/>
)}
{exampleWithSchema.responseBody != null &&
selectedError == null &&
visitDiscriminatedUnion(exampleWithSchema.responseBody, "type")._visit<ReactNode>({
json: (value) => (
<JsonCodeSnippetExample
title={renderResponseTitle(value.statusCode, endpoint.method)}
title={errorSelector}
onClick={(e) => {
e.stopPropagation();
}}
Expand All @@ -124,28 +198,30 @@ const UnmemoizedEndpointContentCodeSnippets: React.FC<EndpointContentCodeSnippet
/>
),
// TODO: support other media types
filename: (value) => (
<AudioExample title={renderResponseTitle(value.statusCode, endpoint.method)} />
),
filename: () => <AudioExample title={errorSelector} />,
stream: (value) => (
<WebSocketMessages
messages={value.value.map((event) => ({
type: undefined,
origin: undefined,
displayName: undefined,
data: event,
}))}
/>
<TitledExample title={errorSelector}>
<WebSocketMessages
messages={value.value.map((event) => ({
type: undefined,
origin: undefined,
displayName: undefined,
data: event,
}))}
/>
</TitledExample>
),
sse: (value) => (
<WebSocketMessages
messages={value.value.map(({ event, data }) => ({
type: event,
origin: undefined,
displayName: undefined,
data,
}))}
/>
<TitledExample title={errorSelector}>
<WebSocketMessages
messages={value.value.map(({ event, data }) => ({
type: event,
origin: undefined,
displayName: undefined,
data,
}))}
/>
</TitledExample>
),
_other: () => (
<FernErrorTag
Expand All @@ -160,13 +236,11 @@ const UnmemoizedEndpointContentCodeSnippets: React.FC<EndpointContentCodeSnippet

export const EndpointContentCodeSnippets = memo(UnmemoizedEndpointContentCodeSnippets);

function renderResponseTitle(statusCode: number, method: string) {
function renderResponseTitle(statusCode: number, method?: APIV1Read.HttpMethod) {
return (
<span className="text-sm px-1 t-muted">
{"Response - "}
<span className="text-intent-success">
{`${statusCode} ${getSuccessMessageForStatus(statusCode, method)}`}
</span>
<span className="inline-flex items-center gap-2">
<StatusCodeTag statusCode={statusCode} />
<span>{getMessageForStatus(statusCode, method)}</span>
</span>
);
}
62 changes: 0 additions & 62 deletions packages/ui/app/src/api-page/endpoints/ErrorCodeSnippetExample.tsx

This file was deleted.

Loading

0 comments on commit 785cba5

Please sign in to comment.