Skip to content

Commit

Permalink
feat: support api requests in bring UI (#6239)
Browse files Browse the repository at this point in the history
Resolves #6131
  • Loading branch information
polamoros authored Apr 15, 2024
1 parent f0b6b11 commit ecc4ffc
Show file tree
Hide file tree
Showing 12 changed files with 860 additions and 118 deletions.
110 changes: 110 additions & 0 deletions apps/wing-console/console/app/demo/main.w
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,113 @@ class WidgetService {
}

new WidgetService();

class ApiUsersService {
api: cloud.Api;
db: cloud.Bucket;

new() {
this.api = new cloud.Api();
this.db = new cloud.Bucket();

this.api.post("/users", inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let input = Json.tryParse(request.body ?? "") ?? "";
let name = input.tryGet("name")?.tryAsStr() ?? "";
if name == "" {
return cloud.ApiResponse {
status: 400,
body: "Body parameter 'name' is required"
};
}
this.db.put("user-{name}", Json.stringify(input));
return cloud.ApiResponse {
status: 200,
body: Json.stringify(input)
};
});
this.api.get("/users", inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let name = request.query.tryGet("name") ?? "";

if name != "" {
try {
return cloud.ApiResponse {
status: 200,
body: this.db.get("user-{name}")
};
} catch {
return cloud.ApiResponse {
status: 404,
body: "User not found"
};
}
}

return cloud.ApiResponse {
status: 200,
body: Json.stringify(this.db.list())
};
});

new ui.HttpClient(
"Test HttpClient UI component",
inflight () => {
return this.api.url;
},
inflight () => {
return Json.stringify({
"paths": {
"/users": {
"post": {
"summary": "Create a new user",
"parameters": [
{
"in": "header",
"name": "cookie",
},
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"name",
],
"properties": {
"name": {
"type": "string",
"description": "The name of the user"
},
"email": {
"type": "string",
"description": "The email of the user",
}
}
}
}
}
},
},
"get": {
"summary": "List all widgets",
"parameters": [
{
"in": "query",
"name": "name",
"schema": {
"type": "string"
},
"description": "The name of the user"
}
],
}
},
}
});
}
);
}
}

new ApiUsersService();
11 changes: 8 additions & 3 deletions apps/wing-console/console/design-system/src/key-value-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import { useTheme } from "./theme-provider.js";

export interface KeyValueItem {
key: string;
value: string;
value?: string;
type?: string;
required?: boolean;
description?: string;
}

export interface UseKeyValueListOptions {
Expand Down Expand Up @@ -159,9 +162,11 @@ export const KeyValueList = ({
/>

<Combobox
placeholder={valuePlaceholder}
placeholder={`${item.type ?? valuePlaceholder} ${
item.required === true ? " (required)" : ""
}${item.description ? ` - ${item.description}` : ""}`}
items={comboboxValues}
value={item.value}
value={item.value ?? ""}
onChange={(value) => {
onItemChange(index, {
key: item.key,
Expand Down
27 changes: 26 additions & 1 deletion apps/wing-console/console/server/src/router/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import type { HttpClient } from "@winglang/sdk/lib/ui/http-client.js";
import uniqby from "lodash.uniqby";
import { z } from "zod";

Expand Down Expand Up @@ -487,6 +488,31 @@ export const createAppRouter = () => {
value: await client.invoke(""),
};
}),
"app.getResourceUiHttpClient": createProcedure
.input(
z.object({
getUrlResourcePath: z.string(),
getApiSpecResourcePath: z.string(),
}),
)
.query(async ({ input, ctx }) => {
const simulator = await ctx.simulator();
const getUrlClient = simulator.getResource(
input.getUrlResourcePath,
) as IFunctionClient;

const url = await getUrlClient.invoke("");

const getApiSpecClient = simulator.getResource(
input.getApiSpecResourcePath,
) as IFunctionClient;
const openApiSpec = await getApiSpecClient.invoke("");

return {
url: url,
openApiSpec: JSON.parse(openApiSpec ?? "{}"),
};
}),

"app.invokeResourceUiButton": createProcedure
.input(
Expand All @@ -501,7 +527,6 @@ export const createAppRouter = () => {
) as IFunctionClient;
await client.invoke("");
}),

"app.analytics": createProcedure.query(async ({ ctx }) => {
const requireSignIn = (await ctx.requireSignIn?.()) ?? false;
if (requireSignIn) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { OpenApiSpec } from "@wingconsole/server/src/wingsdk";
import { memo, useCallback, useContext, useState } from "react";

import { AppContext } from "../AppContext.js";
import { trpc } from "../services/trpc.js";
import { useApi } from "../services/use-api.js";
import type { ApiResponse } from "../shared/api.js";
import { ApiInteraction } from "../ui/api-interaction.js";
Expand All @@ -12,27 +13,25 @@ export interface ApiViewProps {

export const ApiInteractionView = memo(({ resourcePath }: ApiViewProps) => {
const { appMode } = useContext(AppContext);
const [schemaData, setSchemaData] = useState<OpenApiSpec>();

const [apiResponse, setApiResponse] = useState<ApiResponse>();
const onFetchDataUpdate = useCallback(
(data: ApiResponse) => setApiResponse(data),
[],
);
const onSchemaDataUpdate = useCallback(
(data: OpenApiSpec) => setSchemaData(data),
[],
);

const schema = trpc["api.schema"].useQuery({ resourcePath });

const { isLoading, callFetch } = useApi({
resourcePath,
onSchemaDataUpdate,
onFetchDataUpdate,
});

return (
<ApiInteraction
resourceId={resourcePath}
appMode={appMode}
schemaData={schemaData}
url={schema.data?.url}
openApiSpec={schema.data?.openApiSpec as OpenApiSpec}
callFetch={callFetch}
isLoading={isLoading}
apiResponse={apiResponse}
Expand Down
23 changes: 3 additions & 20 deletions apps/wing-console/console/ui/src/services/use-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,16 @@ import type { ApiRequest } from "../shared/api.js";
import { trpc } from "./trpc.js";

export interface UseApiOptions {
resourcePath: string;
onFetchDataUpdate: (data: any) => void;
onSchemaDataUpdate: (data: any) => void;
}

export const useApi = ({
resourcePath,
onFetchDataUpdate,
onSchemaDataUpdate,
}: UseApiOptions) => {
export const useApi = ({ onFetchDataUpdate }: UseApiOptions) => {
const fetch = trpc["api.fetch"].useMutation();
const schema = trpc["api.schema"].useQuery({ resourcePath });

useEffect(() => {
// if (!fetch.data?.textResponse) {
// return;
// }
onFetchDataUpdate(fetch.data);
}, [fetch.data, onFetchDataUpdate]);

useEffect(() => {
if (!schema.data) {
return;
}
onSchemaDataUpdate(schema.data);
}, [onSchemaDataUpdate, schema.data]);

const callFetch = useCallback(
async ({ url, route, method, variables, headers, body }: ApiRequest) => {
if (!url || !method || !route) {
Expand All @@ -50,8 +33,8 @@ export const useApi = ({
);

const isLoading = useMemo(() => {
return fetch.isLoading || schema.isLoading;
}, [fetch.isLoading, schema.isLoading]);
return fetch.isLoading;
}, [fetch.isLoading]);

return {
isLoading,
Expand Down
103 changes: 102 additions & 1 deletion apps/wing-console/console/ui/src/shared/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { KeyValueItem } from "@wingconsole/design-system";
import type { OpenApiSpec } from "@wingconsole/server/src/wingsdk";

export const HTTP_METHODS = [
Expand Down Expand Up @@ -83,6 +82,11 @@ export const getHeaderValues = (header: string): string[] => {

export const getRoutesFromOpenApi = (openApi: OpenApiSpec): ApiRoute[] => {
let routes: ApiRoute[] = [];

if (!openApi?.paths) {
return routes;
}

for (const route of Object.keys(openApi.paths)) {
const methods = Object.keys(openApi.paths[route]);
for (const method of methods) {
Expand All @@ -94,3 +98,100 @@ export const getRoutesFromOpenApi = (openApi: OpenApiSpec): ApiRoute[] => {
}
return routes;
};

export interface Parameter {
key: string;
value: string;
type: string;
required: boolean;
description?: string;
}

export interface GetParametersFromOpenApiProps {
path: string;
method?: string;
openApi: OpenApiSpec;
type: "query" | "header" | "path";
}

export const getParametersFromOpenApi = ({
path,
method = "",
openApi,
type,
}: GetParametersFromOpenApiProps): Parameter[] => {
try {
if (!openApi?.paths[path]) {
return [];
}
const pathParameters = openApi.paths[path].parameters;
const methodParameters =
openApi.paths[path][method.toLowerCase()]?.parameters;

const parametersList = [
...(pathParameters || []),
...(methodParameters || []),
];

let parameters = [];
for (const parameter of parametersList) {
if (parameter.in === type) {
const required = parameter.required || false;
const type = parameter.schema?.type || "string";
parameters.push({
key: parameter.name,
value: "",
type: type as string,
required: required,
description: parameter.description,
});
}
}

return parameters;
} catch (error) {
console.log(error);
return [];
}
};

export const getRequestBodyFromOpenApi = (
path: string,
method: string,
openApi: OpenApiSpec,
): Record<string, string> | undefined => {
try {
const requestBody =
openApi?.paths[path]?.[method.toLowerCase()]?.requestBody;
if (!requestBody) {
return undefined;
}

const jsonContentType = requestBody.content?.["application/json"];
if (!jsonContentType) {
return undefined;
}

const schema = jsonContentType.schema;
if (!schema) {
return undefined;
}

const bodyProperties = schema.properties;

let response = {} as Record<string, string>;
for (const key in bodyProperties) {
const type = bodyProperties[key].type;
const required = schema.required?.includes(key) ? "required" : "optional";
const description = bodyProperties[key].description;
response[key] = `${type} (${required}) ${
description ? `- ${description}` : ""
}`;
}

return response;
} catch (error) {
console.log(error);
return {};
}
};
Loading

0 comments on commit ecc4ffc

Please sign in to comment.