Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: api playground state injection #1827

Merged
merged 2 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 52 additions & 12 deletions packages/ui/app/src/atoms/playground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ import {
type PlaygroundWebSocketRequestFormState,
} from "../playground/types";
import {
getInitialEndpointRequestFormState,
getInitialEndpointRequestFormStateWithExample,
getInitialWebSocketRequestFormState,
} from "../playground/utils";
import { pascalCaseHeaderKeys } from "../playground/utils/header-key-case";
import { FERN_USER_ATOM } from "./auth";
import { FEATURE_FLAGS_ATOM } from "./flags";
import { useAtomEffect } from "./hooks";
import { HEADER_HEIGHT_ATOM } from "./layout";
Expand Down Expand Up @@ -177,7 +178,12 @@ export const PLAYGROUND_AUTH_STATE_ATOM = atomWithStorageValidation<PlaygroundAu
);

export const PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM = atom(
(get) => get(PLAYGROUND_AUTH_STATE_ATOM).bearerAuth ?? PLAYGROUND_AUTH_STATE_BEARER_TOKEN_INITIAL,
(get) => ({
token:
get(PLAYGROUND_AUTH_STATE_ATOM).bearerAuth?.token ??
get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.bearer_token ??
"",
}),
(_get, set, update: SetStateAction<PlaygroundAuthStateBearerToken>) => {
set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({
...prev,
Expand All @@ -190,7 +196,12 @@ export const PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM = atom(
);

export const PLAYGROUND_AUTH_STATE_HEADER_ATOM = atom(
(get) => get(PLAYGROUND_AUTH_STATE_ATOM).header ?? PLAYGROUND_AUTH_STATE_HEADER_INITIAL,
(get) => ({
headers: pascalCaseHeaderKeys({
...(get(PLAYGROUND_AUTH_STATE_ATOM).header?.headers ??
get(FERN_USER_ATOM)?.playground?.initial_state?.headers),
}),
}),
(_get, set, update: SetStateAction<PlaygroundAuthStateHeader>) => {
set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({
...prev,
Expand All @@ -200,7 +211,16 @@ export const PLAYGROUND_AUTH_STATE_HEADER_ATOM = atom(
);

export const PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM = atom(
(get) => get(PLAYGROUND_AUTH_STATE_ATOM).basicAuth ?? PLAYGROUND_AUTH_STATE_BASIC_AUTH_INITIAL,
(get) => ({
username:
get(PLAYGROUND_AUTH_STATE_ATOM).basicAuth?.username ??
get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.basic?.username ??
"",
password:
get(PLAYGROUND_AUTH_STATE_ATOM).basicAuth?.password ??
get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.basic?.password ??
"",
}),
(_get, set, update: SetStateAction<PlaygroundAuthStateBasicAuth>) => {
set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({
...prev,
Expand Down Expand Up @@ -265,6 +285,8 @@ export function useSetAndOpenPlayground(): (node: FernNavigation.NavigationNodeA
return;
}

const playgroundInitialState = get(FERN_USER_ATOM)?.playground?.initial_state;

if (node.type === "endpoint") {
const context = createEndpointContext(node, definition);

Expand All @@ -277,7 +299,11 @@ export function useSetAndOpenPlayground(): (node: FernNavigation.NavigationNodeA

set(
formStateAtom,
getInitialEndpointRequestFormStateWithExample(context, context.endpoint.examples?.[0]),
getInitialEndpointRequestFormStateWithExample(
context,
context.endpoint.examples?.[0],
playgroundInitialState,
),
);
} else if (node.type === "webSocket") {
const context = createWebSocketContext(node, definition);
Expand All @@ -290,7 +316,7 @@ export function useSetAndOpenPlayground(): (node: FernNavigation.NavigationNodeA
return;
}

set(formStateAtom, getInitialWebSocketRequestFormState(context));
set(formStateAtom, getInitialWebSocketRequestFormState(context, playgroundInitialState));
}
playgroundFormStateFamily.remove(node.id);
},
Expand All @@ -304,9 +330,16 @@ export function usePlaygroundEndpointFormState(
): [PlaygroundEndpointRequestFormState, Dispatch<SetStateAction<PlaygroundEndpointRequestFormState>>] {
const formStateAtom = playgroundFormStateFamily(ctx.node.id);
const formState = useAtomValue(formStateAtom);
const user = useAtomValue(FERN_USER_ATOM);

return [
formState?.type === "endpoint" ? formState : getInitialEndpointRequestFormState(ctx),
formState?.type === "endpoint"
? formState
: getInitialEndpointRequestFormStateWithExample(
ctx,
ctx.endpoint.examples?.[0],
user?.playground?.initial_state,
),
useAtomCallback(
useCallbackOne(
(get, set, update: SetStateAction<PlaygroundEndpointRequestFormState>) => {
Expand All @@ -316,12 +349,16 @@ export function usePlaygroundEndpointFormState(
? update(
currentFormState?.type === "endpoint"
? currentFormState
: getInitialEndpointRequestFormState(ctx),
: getInitialEndpointRequestFormStateWithExample(
ctx,
ctx.endpoint.examples?.[0],
user?.playground?.initial_state,
),
)
: update;
set(formStateAtom, newFormState);
},
[formStateAtom, ctx],
[formStateAtom, ctx, user?.playground?.initial_state],
),
),
];
Expand All @@ -332,9 +369,12 @@ export function usePlaygroundWebsocketFormState(
): [PlaygroundWebSocketRequestFormState, Dispatch<SetStateAction<PlaygroundWebSocketRequestFormState>>] {
const formStateAtom = playgroundFormStateFamily(context.node.id);
const formState = useAtomValue(playgroundFormStateFamily(context.node.id));
const user = useAtomValue(FERN_USER_ATOM);

return [
formState?.type === "websocket" ? formState : getInitialWebSocketRequestFormState(context),
formState?.type === "websocket"
? formState
: getInitialWebSocketRequestFormState(context, user?.playground?.initial_state),
useAtomCallback(
useCallbackOne(
(get, set, update: SetStateAction<PlaygroundWebSocketRequestFormState>) => {
Expand All @@ -344,12 +384,12 @@ export function usePlaygroundWebsocketFormState(
? update(
currentFormState?.type === "websocket"
? currentFormState
: getInitialWebSocketRequestFormState(context),
: getInitialWebSocketRequestFormState(context, user?.playground?.initial_state),
)
: update;
set(formStateAtom, newFormState);
},
[formStateAtom, context],
[formStateAtom, context, user?.playground?.initial_state],
),
),
];
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/app/src/playground/WithLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const WithLabel: FC<PropsWithChildren<WithLabelProps>> = ({
isRequired={!unwrapped.isOptional}
isList={unwrapped.shape.type === "list"}
isBoolean={unwrapped.shape.type === "primitive" && unwrapped.shape.value.type === "boolean"}
typeShorthand={renderTypeShorthandRoot(unwrapped.shape, types)}
typeShorthand={renderTypeShorthandRoot(property.valueShape, types, false, true)}
>
{children}
</WithLabelInternal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useAtomValue } from "jotai";
import { ReactElement } from "react";
import { PLAYGROUND_AUTH_STATE_ATOM } from "../../atoms";
import { PlaygroundAuthState } from "../types";
import { pascalCaseHeaderKey } from "../utils/header-key-case";

interface PlaygroundCardTriggerManualProps {
auth: APIV1Read.ApiAuth;
Expand Down Expand Up @@ -74,7 +75,7 @@ function isAuthed(auth: APIV1Read.ApiAuth, authState: PlaygroundAuthState): bool
bearerAuth: () => !isEmpty(authState.bearerAuth?.token.trim()),
basicAuth: () =>
!isEmpty(authState.basicAuth?.username.trim()) && !isEmpty(authState.basicAuth?.password.trim()),
header: (header) => !isEmpty(authState.header?.headers[header.headerWireValue]?.trim()),
header: (header) => !isEmpty(authState.header?.headers[pascalCaseHeaderKey(header.headerWireValue)]?.trim()),
oAuth: () => {
const authToken =
authState.oauth?.selectedInputMethod === "credentials"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { APIV1Read } from "@fern-api/fdr-sdk/client/types";
import { unknownToString } from "@fern-api/ui-core-utils";
import { atom } from "jotai";
import { useAtom } from "jotai/react";
import type { ReactElement, SetStateAction } from "react";
import { useMemoOne } from "use-memo-one";
import { PLAYGROUND_AUTH_STATE_HEADER_ATOM } from "../../atoms";
import { PasswordInputGroup } from "../PasswordInputGroup";
import { pascalCaseHeaderKey } from "../utils/header-key-case";

export function PlaygroundHeaderAuthForm({
header,
Expand All @@ -17,14 +19,15 @@ export function PlaygroundHeaderAuthForm({
useMemoOne(
() =>
atom(
(get) => get(PLAYGROUND_AUTH_STATE_HEADER_ATOM).headers[header.headerWireValue],
(get) =>
get(PLAYGROUND_AUTH_STATE_HEADER_ATOM).headers[pascalCaseHeaderKey(header.headerWireValue)],
(_get, set, change: SetStateAction<string>) => {
set(PLAYGROUND_AUTH_STATE_HEADER_ATOM, ({ headers }) => ({
headers: {
...headers,
[header.headerWireValue]:
[pascalCaseHeaderKey(header.headerWireValue)]:
typeof change === "function"
? change(headers[header.headerWireValue] ?? "")
? change(headers[pascalCaseHeaderKey(header.headerWireValue)] ?? "")
: change,
},
}));
Expand All @@ -37,12 +40,14 @@ export function PlaygroundHeaderAuthForm({
return (
<li className="-mx-4 space-y-2 p-4">
<label className="inline-flex flex-wrap items-baseline">
<span className="font-mono text-sm">{header.nameOverride ?? header.headerWireValue}</span>
<span className="font-mono text-sm">
{header.nameOverride ?? pascalCaseHeaderKey(header.headerWireValue)}
</span>
</label>
<div>
<PasswordInputGroup
onValueChange={setValue}
value={value}
value={unknownToString(value ?? "")}
autoComplete="off"
data-1p-ignore="true"
disabled={disabled}
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/app/src/playground/code-snippets/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { EndpointContext } from "@fern-api/fdr-sdk/api-definition";
import { toCurlyBraceEndpointPathLiteral } from "@fern-api/fdr-sdk/api-definition";
import { FdrAPI, type APIV1Read } from "@fern-api/fdr-sdk/client/types";
import { SnippetTemplateResolver } from "@fern-api/template-resolver";
import { unknownToString } from "@fern-api/ui-core-utils";
import { UnreachableCaseError } from "ts-essentials";
import { provideRegistryService } from "../../services/registry";
import { PlaygroundAuthState, PlaygroundEndpointRequestFormState } from "../types";
Expand Down Expand Up @@ -182,7 +183,7 @@ export class PlaygroundCodeSnippetResolver {
const headers = { ...this.headers };

// TODO: ensure case insensitivity
if (headers["Content-Type"] === "multipart/form-data") {
if (unknownToString(headers["Content-Type"]).includes("multipart/form-data")) {
delete headers["Content-Type"]; // fetch will set this automatically
}

Expand Down
23 changes: 14 additions & 9 deletions packages/ui/app/src/playground/endpoint/PlaygroundEndpoint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { Loadable, failed, loaded, loading, notStartedLoading } from "@fern-ui/l
import { useEventCallback } from "@fern-ui/react-commons";
import { mapValues } from "es-toolkit/object";
import { SendSolid } from "iconoir-react";
import { useSetAtom } from "jotai";
import { useAtomValue, useSetAtom } from "jotai";
import { ReactElement, useCallback, useState } from "react";
import {
FERN_USER_ATOM,
PLAYGROUND_AUTH_STATE_ATOM,
PLAYGROUND_AUTH_STATE_OAUTH_ATOM,
store,
Expand All @@ -25,27 +26,31 @@ import { executeProxyRest } from "../fetch-utils/executeProxyRest";
import { executeProxyStream } from "../fetch-utils/executeProxyStream";
import type { GrpcProxyRequest, ProxyRequest } from "../types";
import { PlaygroundResponse } from "../types/playgroundResponse";
import {
buildAuthHeaders,
getInitialEndpointRequestFormState,
getInitialEndpointRequestFormStateWithExample,
serializeFormStateBody,
} from "../utils";
import { buildAuthHeaders, getInitialEndpointRequestFormStateWithExample, serializeFormStateBody } from "../utils";
import { usePlaygroundBaseUrl } from "../utils/select-environment";
import { PlaygroundEndpointContent } from "./PlaygroundEndpointContent";
import { PlaygroundEndpointPath } from "./PlaygroundEndpointPath";

export const PlaygroundEndpoint = ({ context }: { context: EndpointContext }): ReactElement => {
const user = useAtomValue(FERN_USER_ATOM);
const { node, endpoint, auth } = context;

const [formState, setFormState] = usePlaygroundEndpointFormState(context);

const resetWithExample = useEventCallback(() => {
setFormState(getInitialEndpointRequestFormStateWithExample(context, context.endpoint.examples?.[0]));
setFormState(
getInitialEndpointRequestFormStateWithExample(
context,
context.endpoint.examples?.[0],
user?.playground?.initial_state,
),
);
});

const resetWithoutExample = useEventCallback(() => {
setFormState(getInitialEndpointRequestFormState(context));
setFormState(
getInitialEndpointRequestFormStateWithExample(context, undefined, user?.playground?.initial_state),
);
});

const basePath = useBasePath();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { EndpointContext } from "@fern-api/fdr-sdk/api-definition";
import { PropertyKey, type EndpointContext } from "@fern-api/fdr-sdk/api-definition";
import { EMPTY_ARRAY, visitDiscriminatedUnion } from "@fern-api/ui-core-utils";
import { Dispatch, FC, SetStateAction, useCallback, useMemo } from "react";
import { PlaygroundFileUploadForm } from "../form/PlaygroundFileUploadForm";
import { PlaygroundObjectForm } from "../form/PlaygroundObjectForm";
import { PlaygroundObjectPropertiesForm } from "../form/PlaygroundObjectPropertyForm";
import { PlaygroundEndpointRequestFormState, PlaygroundFormStateBody } from "../types";
import { pascalCaseHeaderKey } from "../utils/header-key-case";
import { PlaygroundEndpointAliasForm } from "./PlaygroundEndpointAliasForm";
import { PlaygroundEndpointFormSection } from "./PlaygroundEndpointFormSection";
import { PlaygroundEndpointMultipartForm } from "./PlaygroundEndpointMultipartForm";
Expand Down Expand Up @@ -104,7 +105,10 @@ export const PlaygroundEndpointForm: FC<PlaygroundEndpointFormProps> = ({
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title="Headers">
<PlaygroundObjectPropertiesForm
id="header"
properties={headers}
properties={headers.map((header) => ({
...header,
key: PropertyKey(pascalCaseHeaderKey(header.key)),
}))}
extraProperties={undefined}
onChange={setHeaders}
value={formState?.headers}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,12 @@ export const PlaygroundObjectPropertiesForm = memo<PlaygroundObjectPropertiesFor
type: "value",
value: property.key,
label: property.key,
helperText: renderTypeShorthandRoot(property.valueShape, types),
helperText: renderTypeShorthandRoot(
{ type: "optional", shape: property.valueShape, default: undefined },
types,
false,
true,
),
labelClassName: "font-mono",
tooltip: property.description != null ? <Markdown size="xs" mdx={property.description} /> : undefined,
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/app/src/playground/types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const PlaygroundAuthStateBasicAuthSchema = z.strictObject({
export type PlaygroundAuthStateBasicAuth = z.infer<typeof PlaygroundAuthStateBasicAuthSchema>;
export const PLAYGROUND_AUTH_STATE_BASIC_AUTH_INITIAL: PlaygroundAuthStateBasicAuth = { username: "", password: "" };

export const PlaygroundAuthStateOAuthSchema = z.strictObject({
export const PlaygroundAuthStateOAuthSchema = z.object({
clientId: z.string(),
clientSecret: z.string(),
accessToken: z.string(),
Expand Down
19 changes: 9 additions & 10 deletions packages/ui/app/src/playground/utils/auth-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedU
import { decodeJwt } from "jose";
import { noop } from "ts-essentials";
import { PlaygroundAuthState } from "../types";
import { pascalCaseHeaderKey } from "./header-key-case";
import {
OAuthClientCredentialReferencedEndpointLoginFlowProps,
oAuthClientCredentialReferencedEndpointLoginFlow,
Expand Down Expand Up @@ -30,11 +31,13 @@ export function buildAuthHeaders(
headers["Authorization"] = `Bearer ${token}`;
},
header: (header) => {
let value = authState.header?.headers[header.headerWireValue] ?? "";
// pluck the value from the headers object (avoid inheriting all the other headers)
let value = authState.header?.headers[pascalCaseHeaderKey(header.headerWireValue)] ?? "";
if (redacted) {
value = obfuscateSecret(value);
}
headers[header.headerWireValue] = header.prefix != null ? `${header.prefix} ${value}` : value;
headers[pascalCaseHeaderKey(header.headerWireValue)] =
header.prefix != null ? `${header.prefix} ${value}` : value;
},
basicAuth: () => {
const username = authState.basicAuth?.username ?? "";
Expand Down Expand Up @@ -77,14 +80,10 @@ export function buildAuthHeaders(
} catch {}
}

const tokenPrefix = authState.oauth?.tokenPrefix?.length
? authState.oauth.tokenPrefix
: "Bearer";
if (redacted) {
headers["Authorization"] = `${tokenPrefix} ${obfuscateSecret(token)}`;
} else {
headers["Authorization"] = `${tokenPrefix} ${token}`;
}
const tokenPrefix = authState.oauth?.tokenPrefix || "Bearer";

headers["Authorization"] =
`${tokenPrefix.length ? `${tokenPrefix} ` : ""}${redacted ? obfuscateSecret(token) : token}`;
},
});
},
Expand Down
Loading
Loading