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: support api requests in bring UI #6239

Merged
merged 23 commits into from
Apr 15, 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
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
Loading