Skip to content

Commit

Permalink
demo: grpc playground hack (#1568)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex McKinney <[email protected]>
Co-authored-by: dsinghvi <[email protected]>
  • Loading branch information
3 people authored Oct 1, 2024
1 parent 593d8ce commit 36adac2
Show file tree
Hide file tree
Showing 15 changed files with 413 additions and 57 deletions.
24 changes: 12 additions & 12 deletions fern/apis/proxy/generators.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
default-group: local
groups:
sdk:
generators: []
# - name: fernapi/fern-typescript-node-sdk
# version: 0.17.1
# output:
# location: npm
# url: npm.buildwithfern.com
# package-name: "@fern-fern/proxy-sdk"
# config:
# skipResponseValidation: true
# outputEsm: true
# noSerdeLayer: true
# noOptionalProperties: true
generators:
- name: fernapi/fern-typescript-node-sdk
version: 0.40.7
output:
location: npm
url: npm.buildwithfern.com
package-name: "@fern-fern/proxy-sdk"
config:
skipResponseValidation: true
outputEsm: true
noSerdeLayer: true
noOptionalProperties: true

local:
generators:
Expand Down
1 change: 1 addition & 0 deletions packages/ui/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@fern-ui/next-seo": "workspace:*",
"@fern-ui/react-commons": "workspace:*",
"@fern-ui/search-utils": "workspace:*",
"@fern-fern/proxy-sdk": "0.0.21",
"@inkeep/widgets": "^0.2.288",
"@next/third-parties": "14.2.9",
"@radix-ui/colors": "^3.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
MOBILE_SIDEBAR_ENABLED_ATOM,
store,
useAtomEffect,
useFeatureFlags,
} from "../../atoms";
import { useHref } from "../../hooks/useHref";
import { ResolvedEndpointDefinition, ResolvedError, ResolvedTypeDefinition } from "../../resolver/types";
Expand Down Expand Up @@ -128,8 +129,13 @@ export const EndpointContent = memo<EndpointContent.Props>((props) => {
return endpoint.examples.filter((e) => e.responseStatusCode === selectedError.statusCode);
}, [endpoint.examples, selectedError]);

// TODO: remove after pinecone demo
const { grpcEndpoints } = useFeatureFlags();
const [contentType, setContentType] = useState<string | undefined>(endpoint.requestBody?.contentType);
const clients = useMemo(() => generateCodeExamples(examples), [examples]);
const clients = useMemo(
() => generateCodeExamples(examples, grpcEndpoints?.includes(endpoint.id)),
[examples, grpcEndpoints, endpoint.id],
);
const [selectedLanguage, setSelectedLanguage] = useAtom(FERN_LANGUAGE_ATOM);
const [selectedClient, setSelectedClient] = useState<CodeExample>(() => {
const curlExample = clients[0]?.examples[0];
Expand Down
56 changes: 34 additions & 22 deletions packages/ui/app/src/api-reference/examples/code-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,48 @@ export interface CodeExampleGroup {
}

// key is the language
export function generateCodeExamples(examples: ResolvedExampleEndpointCall[]): CodeExampleGroup[] {
export function generateCodeExamples(
examples: ResolvedExampleEndpointCall[],
grpc: boolean = false,
): CodeExampleGroup[] {
const codeExamples = new Map<string, CodeExample[]>();
examples.forEach((example, i) => {
example.snippets.forEach((snippet, j) => {
codeExamples.set(snippet.language, [
...(codeExamples.get(snippet.language) ?? []),
{
key: `${snippet.language}-${i}/${j}`,
exampleIndex: i,
language: snippet.language,
name: snippet.name ?? example.name ?? `Example ${i + 1}`,
code: snippet.code,
// hast: snippet.hast,
install: snippet.install,
exampleCall: example,
},
]);
if (!grpc || snippet.language !== "curl") {
codeExamples.set(snippet.language, [
...(codeExamples.get(snippet.language) ?? []),
{
key: `${snippet.language}-${i}/${j}`,
exampleIndex: i,
language: snippet.language,
name: snippet.name ?? example.name ?? `Example ${i + 1}`,
code: snippet.code,
// hast: snippet.hast,
install: snippet.install,
exampleCall: example,
},
]);
}
});
});

// always keep curl at the top
const curlExamples = codeExamples.get("curl");
codeExamples.delete("curl");
return [
{
language: "curl",
languageDisplayName: "cURL",
icon: getIconForClient("curl"),
examples: [...(curlExamples ?? [])],
},

// TODO: remove after pinecone examples
const examplesByLanguage = grpc
? []
: [
{
language: "curl",
languageDisplayName: "cURL",
icon: getIconForClient("curl"),
examples: [...(curlExamples ?? [])],
},
];

return examplesByLanguage.concat([
...sortBy(
Array.from(codeExamples.entries()).map(([language, examples]) => ({
language,
Expand All @@ -60,7 +72,7 @@ export function generateCodeExamples(examples: ResolvedExampleEndpointCall[]): C
})),
"language",
),
];
]);
}

function getIconForClient(clientId: string) {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/app/src/atoms/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const DEFAULT_FEATURE_FLAGS: FeatureFlags = {
isFileForgeHackEnabled: false,
is404PageHidden: false,
isNewSearchExperienceEnabled: false,
// TODO: remove this after pinecone demo, this is a temporary flag
grpcEndpoints: [],
};

export const EMPTY_ANALYTICS_CONFIG: DocsV1Read.AnalyticsConfig = {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/app/src/atoms/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export interface FeatureFlags {
isFileForgeHackEnabled: boolean;
is404PageHidden: boolean;
isNewSearchExperienceEnabled: boolean;
// TODO: remove this after pinecone demo, this is a temporary flag
grpcEndpoints: readonly string[];
}

export interface NavigationProps {
Expand Down
77 changes: 72 additions & 5 deletions packages/ui/app/src/playground/endpoint/PlaygroundEndpoint.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { FernProxyClient } from "@fern-fern/proxy-sdk";
import { FernTooltipProvider } from "@fern-ui/components";
import { unknownToString } from "@fern-ui/core-utils";
import { Loadable, failed, loaded, loading, notStartedLoading } from "@fern-ui/loadable";
import { useEventCallback } from "@fern-ui/react-commons";
import { SendSolid } from "iconoir-react";
import { useSetAtom } from "jotai";
import { mapValues } from "lodash-es";
import { FC, ReactElement, useCallback, useState } from "react";
import { FC, ReactElement, useCallback, useMemo, useState } from "react";
import { captureSentryError } from "../../analytics/sentry";
import {
PLAYGROUND_AUTH_STATE_ATOM,
Expand All @@ -21,10 +22,11 @@ import { useApiRoute } from "../../hooks/useApiRoute";
import { usePlaygroundSettings } from "../../hooks/usePlaygroundSettings";
import { getAppBuildwithfernCom } from "../../hooks/useStandardProxyEnvironment";
import { ResolvedEndpointDefinition, ResolvedTypeDefinition, resolveEnvironment } from "../../resolver/types";
import { executeGrpc } from "../fetch-utils/executeGrpc";
import { executeProxyFile } from "../fetch-utils/executeProxyFile";
import { executeProxyRest } from "../fetch-utils/executeProxyRest";
import { executeProxyStream } from "../fetch-utils/executeProxyStream";
import type { ProxyRequest } from "../types";
import type { GrpcProxyRequest, ProxyRequest } from "../types";
import { PlaygroundResponse } from "../types/playgroundResponse";
import {
buildAuthHeaders,
Expand Down Expand Up @@ -53,14 +55,21 @@ export const PlaygroundEndpoint: FC<PlaygroundEndpointProps> = ({ endpoint, type
});

const basePath = useBasePath();
const { usesApplicationJsonInFormDataValue, proxyShouldUseAppBuildwithfernCom } = useFeatureFlags();
const { usesApplicationJsonInFormDataValue, proxyShouldUseAppBuildwithfernCom, grpcEndpoints } = useFeatureFlags();
const [response, setResponse] = useState<Loadable<PlaygroundResponse>>(notStartedLoading());

const proxyBasePath = proxyShouldUseAppBuildwithfernCom ? getAppBuildwithfernCom() : basePath;
const proxyEnvironment = useApiRoute("/api/fern-docs/proxy", { basepath: proxyBasePath });
const uploadEnvironment = useApiRoute("/api/fern-docs/upload", { basepath: proxyBasePath });
const playgroundEnvironment = usePlaygroundEnvironment();

// TODO: remove potentially
const grpcClient = useMemo(() => {
return new FernProxyClient({
environment: "https://kmxxylsbwyu2f4x7rbhreris3i0zfbys.lambda-url.us-east-1.on.aws/",
});
}, []);

const setOAuthValue = useSetAtom(PLAYGROUND_AUTH_STATE_OAUTH_ATOM);

const sendRequest = useCallback(async () => {
Expand Down Expand Up @@ -176,6 +185,62 @@ export const PlaygroundEndpoint: FC<PlaygroundEndpointProps> = ({ endpoint, type
setOAuthValue,
]);

// Figure out if GRPC endpoint
const sendGrpcRequest = useCallback(async () => {
if (endpoint == null) {
return;
}
setResponse(loading());
try {
const authHeaders = buildAuthHeaders(
endpoint.auth,
store.get(PLAYGROUND_AUTH_STATE_ATOM),
{
redacted: false,
},
{
formState,
endpoint,
proxyEnvironment,
playgroundEnvironment,
setValue: setOAuthValue,
},
);
const headers = {
...authHeaders,
...mapValues(formState.headers ?? {}, (value) => unknownToString(value)),
};

const req: GrpcProxyRequest = {
url: buildEndpointUrl(endpoint, formState, playgroundEnvironment),
endpointId: endpoint.id,
headers,
body: await serializeFormStateBody(
uploadEnvironment,
endpoint.requestBody?.shape,
formState.body,
usesApplicationJsonInFormDataValue,
),
};

const res = await executeGrpc(grpcClient, req);
setResponse(loaded(res));
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
setResponse(failed(e));
}
}, [
endpoint,
formState,
proxyEnvironment,
uploadEnvironment,
usesApplicationJsonInFormDataValue,
playgroundEnvironment,
setOAuthValue,
grpcClient,
]);

const selectedEnvironmentId = useSelectedEnvironmentId();

const settings = usePlaygroundSettings();
Expand All @@ -187,7 +252,8 @@ export const PlaygroundEndpoint: FC<PlaygroundEndpointProps> = ({ endpoint, type
<PlaygroundEndpointPath
method={endpoint.method}
formState={formState}
sendRequest={sendRequest}
// TODO: Remove this after pinecone demo, this is a temporary flag
sendRequest={grpcEndpoints?.includes(endpoint.id) ? sendGrpcRequest : sendRequest}
environment={resolveEnvironment(endpoint, selectedEnvironmentId)}
environmentFilters={settings?.environments}
path={endpoint.path}
Expand All @@ -203,7 +269,8 @@ export const PlaygroundEndpoint: FC<PlaygroundEndpointProps> = ({ endpoint, type
resetWithExample={resetWithExample}
resetWithoutExample={resetWithoutExample}
response={response}
sendRequest={sendRequest}
// TODO: Remove this after pinecone demo, this is a temporary flag
sendRequest={grpcEndpoints?.includes(endpoint.id) ? sendGrpcRequest : sendRequest}
types={types}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ReactElement, ReactNode } from "react";
import { useFeatureFlags } from "../../atoms";
import { HorizontalSplitPane, VerticalSplitPane } from "../VerticalSplitPane";

interface PlaygroundEndpointDesktopLayoutProps {
Expand All @@ -14,17 +15,29 @@ export function PlaygroundEndpointDesktopLayout({
requestCard,
responseCard,
}: PlaygroundEndpointDesktopLayoutProps): ReactElement {
const { grpcEndpoints } = useFeatureFlags();

return (
<HorizontalSplitPane rizeBarHeight={scrollAreaHeight} leftClassName="pl-6 pr-1 mt" rightClassName="pl-1">
{form}

<VerticalSplitPane
className="sticky inset-0 pr-6"
style={{ height: scrollAreaHeight }}
aboveClassName={"pt-6 pb-1 flex items-stretch justify-stretch"}
aboveClassName={
// TODO: Remove after pinecone demo
// @ts-expect-error Pinecone hack
grpcEndpoints?.includes(requestCard.props.endpoint.id)
? "py-6 flex items-stretch justify-stretch"
: "pt-6 pb-1 flex items-stretch justify-stretch"
}
belowClassName="pb-6 pt-1 flex items-stretch justify-stretch"
>
{requestCard}
{
// TODO: Remove after pinecone demo
// @ts-expect-error Pinecone hack
grpcEndpoints?.includes(requestCard.props.endpoint.id) ? null : requestCard
}
{responseCard}
</VerticalSplitPane>
</HorizontalSplitPane>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface PlaygroundEndpointRequestCardProps {
export function PlaygroundEndpointRequestCard({
endpoint,
formState,
}: PlaygroundEndpointRequestCardProps): ReactElement {
}: PlaygroundEndpointRequestCardProps): ReactElement | null {
const { isSnippetTemplatesEnabled, isFileForgeHackEnabled } = useFeatureFlags();
const [requestType, setRequestType] = useAtom(PLAYGROUND_REQUEST_TYPE_ATOM);
const setOAuthValue = useSetAtom(PLAYGROUND_AUTH_STATE_OAUTH_ATOM);
Expand All @@ -33,7 +33,6 @@ export function PlaygroundEndpointRequestCard({
<FernCard className="flex min-w-0 flex-1 shrink flex-col overflow-hidden rounded-xl shadow-sm">
<div className="border-default flex h-10 w-full shrink-0 items-center justify-between border-b px-3 py-2">
<span className="t-muted text-xs uppercase">Request</span>

<FernButtonGroup>
<FernButton
onClick={() => setRequestType("curl")}
Expand Down Expand Up @@ -63,7 +62,7 @@ export function PlaygroundEndpointRequestCard({
Python
</FernButton>
</FernButtonGroup>

)
<CopyToClipboardButton
content={() => {
const authState = store.get(PLAYGROUND_AUTH_STATE_ATOM);
Expand Down
37 changes: 37 additions & 0 deletions packages/ui/app/src/playground/fetch-utils/executeGrpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { FernProxyClient } from "@fern-fern/proxy-sdk";
import { GrpcProxyRequest, ProxyResponse } from "../types";
import { PlaygroundResponse } from "../types/playgroundResponse";

export async function executeGrpc(grpcClient: FernProxyClient, req: GrpcProxyRequest): Promise<PlaygroundResponse> {
let time = 0;
let intervalId: any;

function startTimer() {
intervalId = setInterval(() => {
time++;
}, 1000); // 1000 milliseconds = 1 second
}

function stopTimer() {
clearInterval(intervalId);
}

startTimer();
const grpcResponse = await grpcClient.grpc({
baseUrl: req.url,
endpoint: req.endpointId,
headers: req.headers,
body: req.body?.value,
schema: undefined,
});
stopTimer();

return {
type: "json",
...grpcResponse,
contentType: "application/json",
response: grpcResponse.body as ProxyResponse.SerializableBody,
time,
size: String(new TextEncoder().encode(JSON.stringify(grpcResponse.body)).length),
};
}
Loading

0 comments on commit 36adac2

Please sign in to comment.