Skip to content

Commit

Permalink
Merge branch 'main' into ajiang/stream-example
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity committed May 9, 2024
2 parents 3fbb280 + 8103671 commit da59467
Show file tree
Hide file tree
Showing 22 changed files with 372 additions and 137 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ jobs:
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
workingDir: packages/ui/app

fern-generate:
onlyChanged: true

fern-generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CodeSnippetExample, JsonCodeSnippetExample } from "../examples/CodeSnip
import { JsonPropertyPath } from "../examples/JsonPropertyPath";
import type { CodeExample, CodeExampleGroup } from "../examples/code-example";
import { lineNumberOf } from "../examples/utils";
import { getSuccessMessageForStatus } from "../utils/getSuccessMessageForStatus";
import { WebSocketMessages } from "../web-socket/WebSocketMessages";
import { CodeExampleClientDropdown } from "./CodeExampleClientDropdown";
import { EndpointUrlWithOverflow } from "./EndpointUrlWithOverflow";
Expand Down Expand Up @@ -114,7 +115,7 @@ const UnmemoizedEndpointContentCodeSnippets: React.FC<EndpointContentCodeSnippet
visitDiscriminatedUnion(exampleWithSchema.responseBody, "type")._visit<ReactNode>({
json: (value) => (
<JsonCodeSnippetExample
title="Response"
title={renderResponseTitle(value.statusCode, endpoint.method)}
onClick={(e) => {
e.stopPropagation();
}}
Expand All @@ -123,7 +124,9 @@ const UnmemoizedEndpointContentCodeSnippets: React.FC<EndpointContentCodeSnippet
/>
),
// TODO: support other media types
filename: () => <AudioExample title="Response" />,
filename: (value) => (
<AudioExample title={renderResponseTitle(value.statusCode, endpoint.method)} />
),
stream: (value) => (
<WebSocketMessages
messages={value.value.map((event) => ({
Expand Down Expand Up @@ -156,3 +159,14 @@ const UnmemoizedEndpointContentCodeSnippets: React.FC<EndpointContentCodeSnippet
};

export const EndpointContentCodeSnippets = memo(UnmemoizedEndpointContentCodeSnippets);

function renderResponseTitle(statusCode: number, method: string) {
return (
<span className="text-sm px-1 t-muted">
{"Response - "}
<span className="text-intent-success">
{`${statusCode} ${getSuccessMessageForStatus(statusCode, method)}`}
</span>
</span>
);
}
51 changes: 51 additions & 0 deletions packages/ui/app/src/api-page/utils/getSuccessMessageForStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
type StatusCodeMessages = {
[method: string]: string;
};

type SuccessMessages = {
[code: number]: StatusCodeMessages;
};

export const COMMON_SUCCESS_NAMES: SuccessMessages = {
200: {
PUT: "Updated",
DELETE: "Deleted",
default: "Success",
},
201: {
PUT: "Updated",
default: "Created",
},
202: {
default: "Accepted",
},
203: {
default: "Non-Authoritative Information",
},
204: {
default: "No Content",
},
205: {
default: "Reset Content",
},
206: {
default: "Partial Content",
},
207: {
default: "Multi-Status",
},
208: {
default: "Already Reported",
},
226: {
default: "IM Used",
},
};

export function getSuccessMessageForStatus(statusCode: number, method: string): string {
const messages = COMMON_SUCCESS_NAMES[statusCode];
if (!messages) {
return "Success";
}
return messages[method] ?? messages.default;
}
19 changes: 15 additions & 4 deletions packages/ui/app/src/api-playground/PlaygroundEndpoint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export const PlaygroundEndpoint: FC<PlaygroundEndpointProps> = ({
resetWithoutExample,
types,
}): ReactElement => {
const { basePath } = useDocsContext();
const { basePath, domain } = useDocsContext();
const [response, setResponse] = useState<Loadable<PlaygroundResponse>>(notStartedLoading());
// const [, startTransition] = useTransition();

Expand All @@ -192,7 +192,11 @@ export const PlaygroundEndpoint: FC<PlaygroundEndpointProps> = ({
url: buildEndpointUrl(endpoint, formState),
method: endpoint.method,
headers: buildUnredactedHeaders(endpoint, formState),
<<<<<<< HEAD
body: await serializeFormStateBody(endpoint.requestBody?.shape, formState.body, basePath),
=======
body: await serializeFormStateBody(endpoint.requestBody[0]?.shape, formState.body, basePath, domain),
>>>>>>> main
};
if (endpoint.responseBody?.shape.type === "stream") {
const [res, stream] = await executeProxyStream(req, basePath);
Expand Down Expand Up @@ -250,7 +254,7 @@ export const PlaygroundEndpoint: FC<PlaygroundEndpointProps> = ({
},
});
}
}, [basePath, endpoint, formState]);
}, [basePath, domain, endpoint, formState]);

return (
<FernTooltipProvider>
Expand Down Expand Up @@ -289,8 +293,9 @@ async function serializeFormStateBody(
shape: ResolvedHttpRequestBodyShape | undefined,
body: PlaygroundFormStateBody | undefined,
basePath: string | undefined,
domain: string,
): Promise<ProxyRequest.SerializableBody | undefined> {
if (shape == null || body == null || shape.type !== "formData") {
if (shape == null || body == null) {
return undefined;
}

Expand All @@ -316,6 +321,9 @@ async function serializeFormStateBody(
};
break;
case "json": {
if (shape.type !== "formData") {
return undefined;
}
const property = shape.properties.find((p) => p.key === key && p.type === "bodyProperty") as
| ResolvedFormDataRequestProperty.BodyProperty
| undefined;
Expand All @@ -335,7 +343,10 @@ async function serializeFormStateBody(
assertNever(value);
}
}
return { type: "form-data", value: formDataValue };
// this is a hack to allow the API Playground to send JSON blobs in form data
// revert this once we have a better solution
const isJsonBlob = domain.includes("fileforge");
return { type: "form-data", value: formDataValue, isJsonBlob };
}
case "octet-stream":
return { type: "octet-stream", value: await serializeFile(body.value, basePath) };
Expand Down
1 change: 1 addition & 0 deletions packages/ui/app/src/api-playground/types/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export declare namespace ProxyRequest {
export interface SerializableFormData {
type: "form-data";
value: Record<string, SerializableFormDataEntryValue>;
isJsonBlob?: boolean; // this is a hack to allow the API Playground to send JSON blobs in form data
}

export interface SerializableOctetStream {
Expand Down
56 changes: 46 additions & 10 deletions packages/ui/app/src/mdx/base-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,45 +116,81 @@ export const A: FC<AnchorHTMLAttributes<HTMLAnchorElement>> = ({ className, chil
!isValidElement(child)
? child
: isImgElement(child)
? cloneElement<ImgProps>(child, { disableZoom: true })
? cloneElement<ImgProps>(child, { noZoom: true })
: child.type === "img"
? createElement(Image, { ...child.props, disableZoom: true })
? createElement(Image, { ...child.props, noZoom: true })
: child,
)}
</FernLink>
);
};

export interface ImgProps extends ComponentProps<"img"> {
disableZoom?: boolean;
noZoom?: boolean;
}

function isImgElement(element: ReactElement): element is ReactElement<ImgProps> {
return element.type === Image;
}

export const Image: FC<ImgProps> = ({ className, src, height, width, disableZoom, ...rest }) => {
export const Image: FC<ImgProps> = ({ className, src, width: w, height: h, noZoom, style, ...rest }) => {
const { files } = useDocsContext();
// const mounted = useMounted();
// if (!mounted || disableZoom) {
// return <img {...rest} className={cn(className, "max-w-full")} src={src} alt={alt} />;
// }

const fernImageSrc = useMemo((): DocsV1Read.File_ | undefined => {
if (src == null) {
return undefined;
}

// if src starts with `file:`, assume it's a referenced file; fallback to src if not found
if (src.startsWith("file:")) {
const fileId = src.slice(5);
return files[fileId];
return files[fileId] ?? { type: "url", url: src };
}

return { type: "url", url: src };
}, [files, src]);

const width = stripUnits(w);
const height = stripUnits(h);

const fernImage = (
<FernImage
src={fernImageSrc}
width={width}
height={height}
style={{
width: w,
height: h,
...style,
}}
{...rest}
/>
);

if (noZoom) {
return fernImage;
}

return (
<Zoom zoomImg={{ src: fernImageSrc?.url }} classDialog="custom-backdrop">
<FernImage src={fernImageSrc} {...rest} />
{fernImage}
</Zoom>
);
};

// preserves pixel widths and heights, but strips units from other values
function stripUnits(str: string | number | undefined): number | `${number}` | undefined {
if (str == null || typeof str === "number") {
return str;
} else if (/^\d+$/.test(str)) {
// if str is a number, return it as a string
return str as `${number}`;
} else if (/^\d+(\.\d+)?(px)$/.test(str)) {
// if str is a number followed by "px", return the number as a string
return str.slice(0, -2) as `${number}`;
}

// TODO: handle rem

return undefined;
}
2 changes: 1 addition & 1 deletion packages/ui/app/src/resolver/ApiDefinitionResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ export class ApiDefinitionResolver {
this.resolveResponseBodyShape(response.type),
maybeSerializeMdxContent(response.description),
]);
return { shape, description, availability: undefined };
return { shape, description, statusCode: response?.statusCode ?? 200, availability: undefined };
}

// HACKHACK: this handles the case where FernIR is unable to interpret the security scheme for AsyncAPI
Expand Down
5 changes: 4 additions & 1 deletion packages/ui/app/src/resolver/SchemaWithExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,14 @@ export declare namespace ResolvedExampleEndpointResponseWithSchema {
type: "json";
value: unknown | undefined;
schema: ResolvedTypeShape;
statusCode: number;
}

interface Filename {
type: "filename";
value: string | undefined;
schema: APIV1Read.HttpResponseBodyShape.FileDownload;
statusCode: number;
}

interface Stream {
Expand Down Expand Up @@ -223,6 +225,7 @@ function mergeResponseBodySchemaWithExample(
type: "filename",
value: example?.type === "filename" ? example.value : undefined,
schema,
statusCode: responseBody.statusCode,
}),
streamingText: (schema) => {
if (example?.type === "stream") {
Expand Down Expand Up @@ -256,7 +259,7 @@ function mergeResponseBodySchemaWithExample(
return undefined;
}

return { type: "json", schema, value: example.value };
return { type: "json", schema, value: example.value, statusCode: responseBody.statusCode };
},
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ exports[`resolveApiDefinition > should finish resolving 1`] = `
},
"type": "list",
},
"statusCode": 200,
},
"slug": [
"merchants",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/app/src/resolver/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ export interface ResolvedObjectProperty extends WithMetadata {

export interface ResolvedResponseBody extends WithMetadata {
shape: ResolvedHttpResponseBodyShape;
statusCode: number;
}

export type ResolvedEndpointPathParts = ResolvedEndpointPathParts.Literal | ResolvedEndpointPathParts.PathParameter;
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/docs-bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@fern-ui/core-utils": "workspace:*",
"@fern-ui/fdr-utils": "workspace:*",
"@fern-ui/ui": "workspace:*",
"@sentry/nextjs": "^7.105.0",
"@sentry/nextjs": "^7.112.2",
"@vercel/edge-config": "^1.1.0",
"@workos-inc/node": "^6.1.0",
"cssnano": "^6.0.3",
Expand Down
31 changes: 25 additions & 6 deletions packages/ui/docs-bundle/sentry.client.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,25 @@

import * as Sentry from "@sentry/nextjs";

const sentryEnv = process?.env.NEXT_PUBLIC_APPLICATION_ENVIRONMENT ?? "dev";

Sentry.init({
dsn: "https://216ad381a8f652e036b1833af58627e5@o4507138224160768.ingest.us.sentry.io/4507148139495424",
// Do not enable sentry locally
enabled: process.env.NODE_ENV === "production",
environment: process?.env.NEXT_PUBLIC_APPLICATION_ENVIRONMENT ?? "dev",

// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Performance Monitoring
tracesSampleRate: sentryEnv === "dev" ? 0.5 : 0.75, // Capture 75% of the transactions
// Set sampling rate for profiling - this is relative to tracesSampleRate
profilesSampleRate: sentryEnv === "dev" ? 0.5 : 0.75,

sampleRate: 1.0,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,

// Capture all error replays, but only half all others
replaysOnErrorSampleRate: 1.0,
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 1.0,
replaysSessionSampleRate: sentryEnv === "dev" ? 0 : 0.5,

autoSessionTracking: true,

Expand All @@ -35,4 +37,21 @@ Sentry.init({
],
// This option is required for capturing headers and cookies.
sendDefaultPii: true,

beforeSend: (event: Sentry.Event, _: Sentry.EventHint): Sentry.Event | null => {
// Filter out events from privategpt
if (event.request?.url?.includes("privategpt")) {
return null;
}
if (
event.tags != null &&
event.tags["url"] != null &&
typeof event.tags["url"] === "string" &&
event.tags["url"].includes("privategpt")
) {
return null;
}

return event;
},
});
Loading

0 comments on commit da59467

Please sign in to comment.