Skip to content

Commit

Permalink
fix(console): tsoa ui http client is not maintaining state (#6288)
Browse files Browse the repository at this point in the history
Resolves #6287 

## Checklist

- [ ] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted)
- [ ] Description explains motivation and solution
- [ ] Tests added (always)
- [ ] Docs updated (only required for features)
- [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing

*By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
  • Loading branch information
polamoros authored Apr 22, 2024
1 parent 831382e commit eca7c9b
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 95 deletions.
2 changes: 1 addition & 1 deletion apps/wing-console/console/app/demo/main.w
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ class ApiUsersService {
"parameters": [
{
"in": "header",
"name": "cookie",
"name": "accept",
},
],
"requestBody": {
Expand Down
16 changes: 13 additions & 3 deletions apps/wing-console/console/ui/src/features/api-interaction-view.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<ApiResponse>();
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 });
Expand All @@ -35,6 +44,7 @@ export const ApiInteractionView = memo(({ resourcePath }: ApiViewProps) => {
callFetch={callFetch}
isLoading={isLoading}
apiResponse={apiResponse}
resetApiResponse={() => setApiResponse(undefined)}
/>
);
});
176 changes: 94 additions & 82 deletions apps/wing-console/console/ui/src/ui/api-interaction.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { KeyValueItem } from "@wingconsole/design-system";
import {
Attribute,
Button,
Expand Down Expand Up @@ -36,6 +37,7 @@ export interface ApiInteractionProps {
openApiSpec: OpenApiSpec;
callFetch: (data: ApiRequest) => void;
isLoading: boolean;
resetApiResponse?: () => void;
}

export const ApiInteraction = memo(
Expand All @@ -47,6 +49,7 @@ export const ApiInteraction = memo(
url,
openApiSpec,
isLoading,
resetApiResponse = () => {},
}: ApiInteractionProps) => {
const { theme } = useTheme();

Expand All @@ -55,21 +58,23 @@ export const ApiInteraction = memo(
const [routes, setRoutes] = useState<ApiRoute[]>([]);

const [currentHeaderKey, setCurrentHeaderKey] = usePersistentState("");
const [currentHeaderValues, setCurrentHeaderValues] = useState<string[]>(
[],
);
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");

Expand Down Expand Up @@ -150,76 +155,86 @@ 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(
path,
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,
],
);

Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -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,
],
);

Expand All @@ -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]);
}, []);

Check warning on line 342 in apps/wing-console/console/ui/src/ui/api-interaction.tsx

View workflow job for this annotation

GitHub Actions / Test

React Hook useEffect has a missing dependency: 'loadRoutesFromOpenApi'. Either include it or remove the dependency array

// Load the possible values for the current header key
useEffect(() => {
Expand All @@ -340,7 +348,7 @@ export const ApiInteraction = memo(
return;
}
setCurrentHeaderValues(getHeaderValues(currentHeaderKey));
}, [currentHeaderKey]);
}, [currentHeaderKey, setCurrentHeaderValues]);

// Sync the query parameters with the current route.
useEffect(() => {
Expand Down Expand Up @@ -505,7 +513,7 @@ export const ApiInteraction = memo(
},
{
id: "body",
name: `Body ${body || bodyPlaceholder ? "*" : ""}`,
name: `Body ${isBodyEdited ? "*" : ""}`,
panel: (
<div className="pt-2">
<TextArea
Expand All @@ -514,9 +522,13 @@ export const ApiInteraction = memo(
className="text-sm min-h-[6rem]"
placeholder={bodyPlaceholder ?? "Body..."}
value={body}
onInput={(event) =>
setBody(event.currentTarget.value)
}
onInput={(event) => {
const value = event.currentTarget.value;
setBody(value);
setIsBodyEdited(
value !== "" && value !== bodyPlaceholder,
);
}}
/>
</div>
),
Expand Down
Loading

0 comments on commit eca7c9b

Please sign in to comment.