diff --git a/apps/wing-console/console/app/demo/main.w b/apps/wing-console/console/app/demo/main.w index df32ae67cd2..d6cb57b78a7 100644 --- a/apps/wing-console/console/app/demo/main.w +++ b/apps/wing-console/console/app/demo/main.w @@ -254,7 +254,7 @@ class ApiUsersService { "parameters": [ { "in": "header", - "name": "cookie", + "name": "accept", }, ], "requestBody": { diff --git a/apps/wing-console/console/ui/src/features/api-interaction-view.tsx b/apps/wing-console/console/ui/src/features/api-interaction-view.tsx index 0bac5ab408c..43a625335f0 100644 --- a/apps/wing-console/console/ui/src/features/api-interaction-view.tsx +++ b/apps/wing-console/console/ui/src/features/api-interaction-view.tsx @@ -1,4 +1,5 @@ import type { OpenApiSpec } from "@wingconsole/server/src/wingsdk"; +import { createPersistentState } from "@wingconsole/use-persistent-state"; import { memo, useCallback, useContext, useState } from "react"; import { AppContext } from "../AppContext.js"; @@ -13,11 +14,19 @@ export interface ApiViewProps { export const ApiInteractionView = memo(({ resourcePath }: ApiViewProps) => { const { appMode } = useContext(AppContext); + const { usePersistentState } = createPersistentState(resourcePath); - const [apiResponse, setApiResponse] = useState(); + const [apiResponse, setApiResponse] = usePersistentState< + ApiResponse | undefined + >(); const onFetchDataUpdate = useCallback( - (data: ApiResponse) => setApiResponse(data), - [], + (data: ApiResponse) => { + if (!data) { + return; + } + setApiResponse(data); + }, + [setApiResponse], ); const schema = trpc["api.schema"].useQuery({ resourcePath }); @@ -35,6 +44,7 @@ export const ApiInteractionView = memo(({ resourcePath }: ApiViewProps) => { callFetch={callFetch} isLoading={isLoading} apiResponse={apiResponse} + resetApiResponse={() => setApiResponse(undefined)} /> ); }); diff --git a/apps/wing-console/console/ui/src/ui/api-interaction.tsx b/apps/wing-console/console/ui/src/ui/api-interaction.tsx index d38c90fd0d1..1f882b61a39 100644 --- a/apps/wing-console/console/ui/src/ui/api-interaction.tsx +++ b/apps/wing-console/console/ui/src/ui/api-interaction.tsx @@ -1,3 +1,4 @@ +import type { KeyValueItem } from "@wingconsole/design-system"; import { Attribute, Button, @@ -36,6 +37,7 @@ export interface ApiInteractionProps { openApiSpec: OpenApiSpec; callFetch: (data: ApiRequest) => void; isLoading: boolean; + resetApiResponse?: () => void; } export const ApiInteraction = memo( @@ -47,6 +49,7 @@ export const ApiInteraction = memo( url, openApiSpec, isLoading, + resetApiResponse = () => {}, }: ApiInteractionProps) => { const { theme } = useTheme(); @@ -55,21 +58,23 @@ export const ApiInteraction = memo( const [routes, setRoutes] = useState([]); const [currentHeaderKey, setCurrentHeaderKey] = usePersistentState(""); - const [currentHeaderValues, setCurrentHeaderValues] = useState( - [], - ); - const [currentRoute, setCurrentRoute] = usePersistentState(""); - + const [currentHeaderValues, setCurrentHeaderValues] = usePersistentState< + string[] + >([]); const [currentMethod, setCurrentMethod] = usePersistentState("GET"); + const [currentRoute, setCurrentRoute] = usePersistentState(""); + const bodyId = useId(); - const [bodyPlaceholder, setBodyPlaceholder] = useState< + const [isBodyEdited, setIsBodyEdited] = usePersistentState(false); + const [body, setBody] = usePersistentState(""); + const [bodyPlaceholder, setBodyPlaceholder] = usePersistentState< string | undefined >(); - const [body, setBody] = usePersistentState(""); const [currentOptionsTab, setCurrentOptionsTab] = usePersistentState("headers"); + const [currentResponseTab, setCurrentResponseTab] = usePersistentState("body"); @@ -150,57 +155,62 @@ export const ApiInteraction = memo( const loadDataFromOpenApi = useCallback( (path: string, method: string) => { - // Set the headers - const headersFromSpec = getParametersFromOpenApi({ - path: path, - method: method, - openApi: openApiSpec, - type: "header", + setHeaders((headers) => { + const headersFromSpec = getParametersFromOpenApi({ + path: path, + method: method, + openApi: openApiSpec, + type: "header", + }); + const newHeaders = headersFromSpec.filter( + (header) => + !headers.some( + (existingHeader) => existingHeader.key === header.key, + ), + ); + return [ + ...headers.filter((header) => header.value !== ""), + ...newHeaders, + ]; }); - const newHeaders = headersFromSpec.filter( - (header) => - !headers.some( - (existingHeader) => existingHeader.key === header.key, - ), - ); - setHeaders((headers) => [...headers, ...newHeaders]); - - // Set Query Parameters - const queryParametersFromSpec = getParametersFromOpenApi({ - path: path, - method: method, - openApi: openApiSpec, - type: "query", + + setQueryParameters((queryParameters) => { + const queryParametersFromSpec = getParametersFromOpenApi({ + path: path, + method: method, + openApi: openApiSpec, + type: "query", + }); + const newQueryParameters = queryParametersFromSpec.filter( + (parameter) => + !queryParameters.some( + (existingParameter) => existingParameter.key === parameter.key, + ), + ); + return [ + ...queryParameters.filter((parameter) => parameter.value !== ""), + ...newQueryParameters, + ]; }); - const newQueryParameters = queryParametersFromSpec.filter( - (parameter) => - !queryParameters.some( - (existingParameter) => existingParameter.key === parameter.key, - ), - ); - setQueryParameters((queryParameters) => [ - ...queryParameters, - ...newQueryParameters, - ]); - - // Set Path Variables - const variablesFromSpec = getParametersFromOpenApi({ - path: path, - method: method, - openApi: openApiSpec, - type: "path", + setPathVariables((pathVariables) => { + const variablesFromSpec = getParametersFromOpenApi({ + path: path, + method: method, + openApi: openApiSpec, + type: "path", + }); + const newPathVariables = variablesFromSpec.filter( + (variable) => + !pathVariables.some( + (existingVariable) => existingVariable.key === variable.key, + ), + ); + return [ + ...pathVariables.filter((variable) => variable.value !== ""), + ...newPathVariables, + ]; }); - const newPathVariables = variablesFromSpec.filter( - (variable) => - !pathVariables.some( - (existingVariable) => existingVariable.key === variable.key, - ), - ); - setPathVariables((pathVariables) => [ - ...pathVariables, - ...newPathVariables, - ]); // Set the body const bodyFromSpec = getRequestBodyFromOpenApi( @@ -208,18 +218,23 @@ export const ApiInteraction = memo( method, openApiSpec, ); - setBody(JSON.stringify(bodyFromSpec, undefined, 2)); - setBodyPlaceholder(JSON.stringify(bodyFromSpec, undefined, 2)); + const body = bodyFromSpec + ? JSON.stringify(bodyFromSpec, undefined, 2) + : undefined; + + if (!isBodyEdited) { + setBody(body ?? ""); + } + setBodyPlaceholder(body); }, [ openApiSpec, setHeaders, setBody, + isBodyEdited, + setBodyPlaceholder, setQueryParameters, setPathVariables, - queryParameters, - pathVariables, - headers, ], ); @@ -252,18 +267,11 @@ export const ApiInteraction = memo( ); if (!isListedRoute) { - setHeaders([]); - setBody(""); setBodyPlaceholder(undefined); - setPathVariables([]); - setQueryParameters([]); } setQueryParameters(() => { - const newUrlParameters: { - key: string; - value: string; - }[] = []; + const newUrlParameters: KeyValueItem[] = []; for (const [key, value] of urlParameters.entries()) { newUrlParameters.push({ key, @@ -274,10 +282,7 @@ export const ApiInteraction = memo( }); setPathVariables(() => { - const newPathVariables: { - key: string; - value: string; - }[] = []; + const newPathVariables: KeyValueItem[] = []; const matches = newRoute.matchAll(/{(\w+)}/g) || []; for (const match of matches) { @@ -293,18 +298,21 @@ export const ApiInteraction = memo( }); if (isListedRoute && method) { - handleMethodChange(path, method); + setCurrentMethod(method); + loadDataFromOpenApi(path, method); } + resetApiResponse(); }, [ - setHeaders, routes, - setBody, + setBodyPlaceholder, setCurrentRoute, + setCurrentMethod, currentMethod, setPathVariables, setQueryParameters, - handleMethodChange, + loadDataFromOpenApi, + resetApiResponse, ], ); @@ -328,10 +336,10 @@ export const ApiInteraction = memo( handleMethodChange, ]); - // Load the routes from the OpenAPI spec + // load the routes from the open api spec on mount useEffect(() => { loadRoutesFromOpenApi(); - }, [openApiSpec, loadRoutesFromOpenApi]); + }, []); // Load the possible values for the current header key useEffect(() => { @@ -340,7 +348,7 @@ export const ApiInteraction = memo( return; } setCurrentHeaderValues(getHeaderValues(currentHeaderKey)); - }, [currentHeaderKey]); + }, [currentHeaderKey, setCurrentHeaderValues]); // Sync the query parameters with the current route. useEffect(() => { @@ -505,7 +513,7 @@ export const ApiInteraction = memo( }, { id: "body", - name: `Body ${body || bodyPlaceholder ? "*" : ""}`, + name: `Body ${isBodyEdited ? "*" : ""}`, panel: (