From cb17e0f6955baaca4c4aed989d85d061cd50dfda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pol=20Amor=C3=B3s?= Date: Wed, 17 Apr 2024 12:38:29 +0200 Subject: [PATCH] feat: support file browser in bring ui (#6247) Resolves #6198 --------- Signed-off-by: monada-bot[bot] Co-authored-by: Ainvoner Co-authored-by: wingbot <109207340+monadabot@users.noreply.github.com> Co-authored-by: monada-bot[bot] Co-authored-by: Ainvoner <2538825+ainvoner@users.noreply.github.com> --- apps/wing-console/console/app/demo/main.w | 42 ++- .../console/server/src/router/app.ts | 57 +-- .../console/server/src/router/bucket.ts | 2 - .../console/server/src/router/file-browser.ts | 96 +++++ .../console/server/src/router/http-client.ts | 44 +++ .../console/server/src/router/index.ts | 8 + .../console/server/src/router/ui-button.ts | 22 ++ .../console/server/src/router/ui-field.ts | 24 ++ .../src/features/bucket-interaction-view.tsx | 71 +--- .../ui/src/features/file-browser-view.tsx | 94 +++++ .../console/ui/src/services/use-bucket.ts | 16 +- .../src/ui/custom-resource-file-browser.tsx | 150 ++++++++ .../ui/src/ui/custom-resource-http-client.tsx | 62 ++++ .../ui/src/ui/custom-resource-item.tsx | 71 ++++ .../ui/src/ui/custom-resource-ui-button.tsx | 36 ++ .../ui/src/ui/custom-resource-ui-field.tsx | 21 ++ ...ucket-interaction.tsx => file-browser.tsx} | 26 +- .../console/ui/src/ui/resource-metadata.tsx | 156 +------- .../04-standard-library/ui/api-reference.md | 341 ++++++++++++++++++ libs/wingsdk/src/core/tree.ts | 17 +- .../src/ui/file-browser.delete.inflight.ts | 18 + .../src/ui/file-browser.get.inflight.ts | 18 + .../src/ui/file-browser.list.inflight.ts | 17 + .../src/ui/file-browser.put.inflight.ts | 18 + libs/wingsdk/src/ui/file-browser.ts | 199 ++++++++++ libs/wingsdk/src/ui/index.ts | 1 + 26 files changed, 1329 insertions(+), 298 deletions(-) create mode 100644 apps/wing-console/console/server/src/router/file-browser.ts create mode 100644 apps/wing-console/console/server/src/router/http-client.ts create mode 100644 apps/wing-console/console/server/src/router/ui-button.ts create mode 100644 apps/wing-console/console/server/src/router/ui-field.ts create mode 100644 apps/wing-console/console/ui/src/features/file-browser-view.tsx create mode 100644 apps/wing-console/console/ui/src/ui/custom-resource-file-browser.tsx create mode 100644 apps/wing-console/console/ui/src/ui/custom-resource-http-client.tsx create mode 100644 apps/wing-console/console/ui/src/ui/custom-resource-item.tsx create mode 100644 apps/wing-console/console/ui/src/ui/custom-resource-ui-button.tsx create mode 100644 apps/wing-console/console/ui/src/ui/custom-resource-ui-field.tsx rename apps/wing-console/console/ui/src/ui/{bucket-interaction.tsx => file-browser.tsx} (93%) create mode 100644 libs/wingsdk/src/ui/file-browser.delete.inflight.ts create mode 100644 libs/wingsdk/src/ui/file-browser.get.inflight.ts create mode 100644 libs/wingsdk/src/ui/file-browser.list.inflight.ts create mode 100644 libs/wingsdk/src/ui/file-browser.put.inflight.ts create mode 100644 libs/wingsdk/src/ui/file-browser.ts diff --git a/apps/wing-console/console/app/demo/main.w b/apps/wing-console/console/app/demo/main.w index a509e5401af..df32ae67cd2 100644 --- a/apps/wing-console/console/app/demo/main.w +++ b/apps/wing-console/console/app/demo/main.w @@ -1,5 +1,7 @@ bring cloud; bring ex; +bring ui; + // @see https://github.com/winglang/wing/issues/4237 it crashes the Console preview env. //let secret = new cloud.Secret(name: "my-secret"); @@ -8,6 +10,42 @@ let queue = new cloud.Queue(); let api = new cloud.Api(); let counter = new cloud.Counter(initial: 0); +class myBucket { + b: cloud.Bucket; + new() { + this.b = new cloud.Bucket(); + new ui.FileBrowser("File Browser", + { + put: inflight (fileName: str, fileContent:str) => { + this.b.put(fileName, fileContent); + }, + delete: inflight (fileName: str) => { + this.b.delete(fileName); + }, + get: inflight (fileName: str) => { + return this.b.get(fileName); + }, + list: inflight () => {return this.b.list();}, + } + ); + + new cloud.Service( + inflight () => { + this.b.put("hello.txt", "Hello, GET!"); + return inflight () => { + }; + }, + ); + } + pub inflight put(key: str, value: str) { + this.b.put(key, value); + } +} + +let myB = new myBucket() as "MyUIComponentBucket"; +let putfucn = new cloud.Function(inflight () => { + myB.put("test", "Test"); +}) as "PutFileInCustomBucket"; api.get("/test-get", inflight (req: cloud.ApiRequest): cloud.ApiResponse => { bucket.put("hello.txt", "Hello, GET!"); @@ -25,7 +63,8 @@ api.post("/test-post", inflight (req: cloud.ApiRequest): cloud.ApiResponse => { }); let handler = inflight (message: str): str => { - bucket.put("hello.txt", "Hello, {message}!"); + counter.inc(); + bucket.put("hello{counter.peek()}.txt", "Hello, {message}!"); log("Hello, {message}!"); return message; }; @@ -123,7 +162,6 @@ test "Add fixtures" { counter.inc(100); } -bring ui; class WidgetService { data: cloud.Bucket; diff --git a/apps/wing-console/console/server/src/router/app.ts b/apps/wing-console/console/server/src/router/app.ts index 44bb1ab4c27..753d01485ff 100644 --- a/apps/wing-console/console/server/src/router/app.ts +++ b/apps/wing-console/console/server/src/router/app.ts @@ -1,6 +1,5 @@ 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"; @@ -15,7 +14,6 @@ import type { import { buildConstructTreeNodeMap } from "../utils/constructTreeNodeMap.js"; import type { FileLink } from "../utils/createRouter.js"; import { createProcedure, createRouter } from "../utils/createRouter.js"; -import { isTermsAccepted, getLicense } from "../utils/terms-and-conditions.js"; import type { IFunctionClient, Simulator } from "../wingsdk.js"; const isTest = /(\/test$|\/test:([^/\\])+$)/; @@ -470,63 +468,10 @@ export const createAppRouter = () => { return ui as Array<{ kind: string; label: string; - handler: string; + handler: string | Record; }>; }), - "app.getResourceUiField": createProcedure - .input( - z.object({ - resourcePath: z.string(), - }), - ) - .query(async ({ input, ctx }) => { - const simulator = await ctx.simulator(); - const client = simulator.getResource( - input.resourcePath, - ) as IFunctionClient; - return { - 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( - z.object({ - resourcePath: z.string(), - }), - ) - .mutation(async ({ input, ctx }) => { - const simulator = await ctx.simulator(); - const client = simulator.getResource( - input.resourcePath, - ) as IFunctionClient; - await client.invoke(""); - }), "app.analytics": createProcedure.query(async ({ ctx }) => { const requireSignIn = (await ctx.requireSignIn?.()) ?? false; if (requireSignIn) { diff --git a/apps/wing-console/console/server/src/router/bucket.ts b/apps/wing-console/console/server/src/router/bucket.ts index ca37155c4af..06d7a4f7726 100644 --- a/apps/wing-console/console/server/src/router/bucket.ts +++ b/apps/wing-console/console/server/src/router/bucket.ts @@ -1,5 +1,3 @@ -import fs from "node:fs"; - import { z } from "zod"; import { createProcedure, createRouter } from "../utils/createRouter.js"; diff --git a/apps/wing-console/console/server/src/router/file-browser.ts b/apps/wing-console/console/server/src/router/file-browser.ts new file mode 100644 index 00000000000..c5e5813c5ae --- /dev/null +++ b/apps/wing-console/console/server/src/router/file-browser.ts @@ -0,0 +1,96 @@ +import { z } from "zod"; + +import { createProcedure, createRouter } from "../utils/createRouter.js"; +import type { IFunctionClient } from "../wingsdk.js"; + +export const createFileBrowserRouter = () => { + return createRouter({ + "fileBrowser.get": createProcedure + .input( + z.object({ + resourcePath: z.string(), + fileName: z.string(), + }), + ) + .query(async ({ input, ctx }) => { + const simulator = await ctx.simulator(); + const client = simulator.getResource( + input.resourcePath, + ) as IFunctionClient; + return await client.invoke( + JSON.stringify({ fileName: input.fileName }), + ); + }), + + "fileBrowser.list": createProcedure + .input( + z.object({ + resourcePath: z.string(), + }), + ) + .query(async ({ input, ctx }) => { + const simulator = await ctx.simulator(); + const client = simulator.getResource(input.resourcePath); + return (await client.invoke()) as string[]; + }), + + "fileBrowser.put": createProcedure + .input( + z.object({ + resourcePath: z.string(), + fileName: z.string(), + fileContent: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + const simulator = await ctx.simulator(); + const client = simulator.getResource( + input.resourcePath, + ) as IFunctionClient; + return await client.invoke( + JSON.stringify({ + fileName: input.fileName, + fileContent: input.fileContent, + }), + ); + }), + + "fileBrowser.delete": createProcedure + .input( + z.object({ + resourcePath: z.string(), + fileName: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + const simulator = await ctx.simulator(); + const client = simulator.getResource( + input.resourcePath, + ) as IFunctionClient; + return await client.invoke( + JSON.stringify({ + fileName: input.fileName, + }), + ); + }), + + "fileBrowser.download": createProcedure + .input( + z.object({ + resourcePath: z.string(), + fileName: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + const simulator = await ctx.simulator(); + const client = simulator.getResource( + input.resourcePath, + ) as IFunctionClient; + return await client.invoke( + JSON.stringify({ + fileName: input.fileName, + }), + ); + }), + }); +}; diff --git a/apps/wing-console/console/server/src/router/http-client.ts b/apps/wing-console/console/server/src/router/http-client.ts new file mode 100644 index 00000000000..64ff12617de --- /dev/null +++ b/apps/wing-console/console/server/src/router/http-client.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; + +import { createProcedure, createRouter } from "../utils/createRouter.js"; +import type { IFunctionClient } from "../wingsdk.js"; + +export const createHttpClientRouter = () => { + return createRouter({ + "httpClient.getUrl": createProcedure + .input( + z.object({ + resourcePath: z.string(), + }), + ) + .query(async ({ input, ctx }) => { + const simulator = await ctx.simulator(); + const getUrlClient = simulator.getResource( + input.resourcePath, + ) as IFunctionClient; + + const url = await getUrlClient.invoke(""); + return { + url, + }; + }), + + "httpClient.getOpenApiSpec": createProcedure + .input( + z.object({ + resourcePath: z.string(), + }), + ) + .query(async ({ input, ctx }) => { + const simulator = await ctx.simulator(); + const getApiSpecClient = simulator.getResource( + input.resourcePath, + ) as IFunctionClient; + + const openApiSpec = await getApiSpecClient.invoke(""); + return { + openApiSpec: JSON.parse(openApiSpec ?? "{}"), + }; + }), + }); +}; diff --git a/apps/wing-console/console/server/src/router/index.ts b/apps/wing-console/console/server/src/router/index.ts index f3bef3405cf..4a5e645b330 100644 --- a/apps/wing-console/console/server/src/router/index.ts +++ b/apps/wing-console/console/server/src/router/index.ts @@ -6,12 +6,16 @@ import { createBucketRouter } from "./bucket.js"; import { createConfigRouter } from "./config.js"; import { createCounterRouter } from "./counter.js"; import { createEndpointRouter } from "./endpoint.js"; +import { createFileBrowserRouter } from "./file-browser.js"; import { createFunctionRouter } from "./function.js"; +import { createHttpClientRouter } from "./http-client.js"; import { createQueueRouter } from "./queue.js"; import { createRedisRouter } from "./redis.js"; import { createTableRouter } from "./table.js"; import { createTestRouter } from "./test.js"; import { createTopicRouter } from "./topic.js"; +import { createUiButtonRouter } from "./ui-button.js"; +import { createUiFieldRouter } from "./ui-field.js"; import { createUpdaterRouter } from "./updater.js"; import { createWebsiteRouter } from "./website.js"; @@ -33,6 +37,10 @@ export const mergeAllRouters = () => { createWebsiteRouter(), createConfigRouter(), createEndpointRouter(), + createUiButtonRouter(), + createUiFieldRouter(), + createHttpClientRouter(), + createFileBrowserRouter(), ); return { router }; diff --git a/apps/wing-console/console/server/src/router/ui-button.ts b/apps/wing-console/console/server/src/router/ui-button.ts new file mode 100644 index 00000000000..2a237b32862 --- /dev/null +++ b/apps/wing-console/console/server/src/router/ui-button.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +import { createProcedure, createRouter } from "../utils/createRouter.js"; +import type { IFunctionClient } from "../wingsdk.js"; + +export const createUiButtonRouter = () => { + return createRouter({ + "uiButton.invoke": createProcedure + .input( + z.object({ + resourcePath: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + const simulator = await ctx.simulator(); + const client = simulator.getResource( + input.resourcePath, + ) as IFunctionClient; + await client.invoke(""); + }), + }); +}; diff --git a/apps/wing-console/console/server/src/router/ui-field.ts b/apps/wing-console/console/server/src/router/ui-field.ts new file mode 100644 index 00000000000..7d66c6d9d93 --- /dev/null +++ b/apps/wing-console/console/server/src/router/ui-field.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +import { createProcedure, createRouter } from "../utils/createRouter.js"; +import type { IFunctionClient } from "../wingsdk.js"; + +export const createUiFieldRouter = () => { + return createRouter({ + "uiField.get": createProcedure + .input( + z.object({ + resourcePath: z.string(), + }), + ) + .query(async ({ input, ctx }) => { + const simulator = await ctx.simulator(); + const client = simulator.getResource( + input.resourcePath, + ) as IFunctionClient; + return { + value: await client.invoke(""), + }; + }), + }); +}; diff --git a/apps/wing-console/console/ui/src/features/bucket-interaction-view.tsx b/apps/wing-console/console/ui/src/features/bucket-interaction-view.tsx index 7dafa653c32..a72a80084f6 100644 --- a/apps/wing-console/console/ui/src/features/bucket-interaction-view.tsx +++ b/apps/wing-console/console/ui/src/features/bucket-interaction-view.tsx @@ -1,9 +1,9 @@ import type { TreeEntry } from "@wingconsole/design-system"; -import type { FormEventHandler } from "react"; import { memo, useCallback, useMemo, useState } from "react"; import { useBucket } from "../services/use-bucket.js"; -import { BucketInteraction } from "../ui/bucket-interaction.js"; + +import { FileBrowserView } from "./file-browser-view.js"; export interface BucketViewProps { resourcePath: string; @@ -11,80 +11,29 @@ export interface BucketViewProps { export const BucketInteractionView = memo( ({ resourcePath }: BucketViewProps) => { - const [entries, setEntries] = useState([]); - const [selectedEntries, setSelectedEntries] = useState([]); const [currentFile, setCurrentFile] = useState(); - const onFileListChange = useCallback((files: string[]) => { - setEntries( - files.map((file) => ({ - id: file, - name: file, - })) ?? [], - ); - setCurrentFile(undefined); - setSelectedEntries([]); - }, []); - const { uploadFiles, - downloadFile, downloadFiles, deleteFile, currentFileContent, isLoading, + files, } = useBucket({ resourcePath, - onFileListChange, currentFile, }); - const [fileInputId, setFileInputId] = useState(new Date().toString()); - - const uploadSelectedEntries: FormEventHandler = - useCallback( - async (event) => { - await uploadFiles(event.currentTarget.files ?? new FileList()); - // force re-render to avoid problems uploading the same file twice - setFileInputId(new Date().toString()); - }, - [uploadFiles], - ); - - const deleteSelectedEntries = useCallback(async () => { - if (currentFile) { - setCurrentFile(undefined); - } - await deleteFile(selectedEntries); - setSelectedEntries([]); - }, [currentFile, deleteFile, selectedEntries]); - - const downloadSelectedEntries = useCallback(async () => { - await downloadFiles(selectedEntries); - setSelectedEntries([]); - }, [selectedEntries, downloadFiles]); - - const downloadCurrentFile = useCallback(async () => { - if (!currentFile || !currentFileContent) { - return; - } - downloadFile(currentFile, currentFileContent); - }, [currentFile, currentFileContent, downloadFile]); - return ( - ); }, diff --git a/apps/wing-console/console/ui/src/features/file-browser-view.tsx b/apps/wing-console/console/ui/src/features/file-browser-view.tsx new file mode 100644 index 00000000000..197c54d1351 --- /dev/null +++ b/apps/wing-console/console/ui/src/features/file-browser-view.tsx @@ -0,0 +1,94 @@ +import type { TreeEntry } from "@wingconsole/design-system"; +import type { FormEventHandler } from "react"; +import { useCallback, useEffect, useState } from "react"; + +import { FileBrowser } from "../ui/file-browser.js"; + +export interface FileBrowserViewProps { + isLoading: boolean; + files: string[]; + deleteFilesHandler: (files: string[]) => Promise; + downloadFilesHandler: (files: string[]) => Promise; + uploadFilesHandler: (fileList: FileList) => Promise; + currentFileContent: string | undefined; + onCurrentFileChange: (file: string | undefined) => void; +} +export const FileBrowserView = ({ + files, + uploadFilesHandler, + deleteFilesHandler, + downloadFilesHandler, + currentFileContent, + onCurrentFileChange, + isLoading, +}: FileBrowserViewProps) => { + const [fileEntries, setFileEntries] = useState([]); + const [selectedFiles, setSelectedFiles] = useState([]); + const [currentFile, setCurrentFile] = useState(); + const [fileInputId, setFileInputId] = useState(new Date().toString()); + + useEffect(() => { + setFileEntries( + files.map((file) => ({ + id: file, + name: file, + })) ?? [], + ); + }, [files]); + + useEffect(() => { + if (currentFile && !fileEntries.some((file) => file.id === currentFile)) { + setCurrentFile(undefined); + } + }, [fileEntries]); + + useEffect(() => { + onCurrentFileChange(currentFile); + }, [currentFile, onCurrentFileChange]); + + const uploadSelectedFiles: FormEventHandler = useCallback( + async (event) => { + await uploadFilesHandler(event.currentTarget.files ?? new FileList()); + // force re-render to avoid problems uploading the same file twice + setFileInputId(new Date().toString()); + }, + [uploadFilesHandler], + ); + + const deleteSelectedFiles = useCallback(async () => { + if (currentFile) { + setCurrentFile(undefined); + } + await deleteFilesHandler(selectedFiles); + setSelectedFiles([]); + }, [currentFile, deleteFilesHandler, selectedFiles]); + + const downloadSelectedFiles = useCallback(async () => { + await downloadFilesHandler(selectedFiles); + setSelectedFiles([]); + }, [selectedFiles, downloadFilesHandler]); + + const downloadCurrentFile = useCallback(async () => { + if (!currentFile) { + return; + } + void downloadFilesHandler([currentFile]); + }, [currentFile, downloadFilesHandler]); + + return ( + + ); +}; diff --git a/apps/wing-console/console/ui/src/services/use-bucket.ts b/apps/wing-console/console/ui/src/services/use-bucket.ts index 1494e933124..86a3c2b0c7b 100644 --- a/apps/wing-console/console/ui/src/services/use-bucket.ts +++ b/apps/wing-console/console/ui/src/services/use-bucket.ts @@ -7,14 +7,10 @@ import { trpc } from "./trpc.js"; export interface UseBucketOptions { resourcePath: string; - onFileListChange: (files: string[]) => void; currentFile?: string; } -export const useBucket = ({ - resourcePath, - onFileListChange, - currentFile, -}: UseBucketOptions) => { +export const useBucket = ({ resourcePath, currentFile }: UseBucketOptions) => { + const [files, setFiles] = useState([]); const { readBlob } = useUploadFile(); const { download: downloadFileLocally, downloadFiles: downloadFilesLocally } = useDownloadFile(); @@ -23,7 +19,7 @@ export const useBucket = ({ { resourcePath }, { onSuccess(data) { - onFileListChange(data); + setFiles(data); }, }, ); @@ -49,8 +45,9 @@ export const useBucket = ({ }, [currentFileContentQuery.data]); useEffect(() => { - onFileListChange(list.data ?? []); - }, [list.data, onFileListChange]); + if (!list.data) return; + setFiles([...list.data]); + }, [list.data]); const putFile = useCallback( (fileName: string, fileContent: string) => { @@ -130,5 +127,6 @@ export const useBucket = ({ deleteFile, currentFileContent, isLoading, + files, }; }; diff --git a/apps/wing-console/console/ui/src/ui/custom-resource-file-browser.tsx b/apps/wing-console/console/ui/src/ui/custom-resource-file-browser.tsx new file mode 100644 index 00000000000..d68ac88fb2a --- /dev/null +++ b/apps/wing-console/console/ui/src/ui/custom-resource-file-browser.tsx @@ -0,0 +1,150 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; + +import { FileBrowserView } from "../features/file-browser-view.js"; +import { trpc } from "../services/trpc.js"; +import { useDownloadFile } from "../shared/use-download-file.js"; +import { useUploadFile } from "../shared/use-upload-file.js"; + +export interface CustomResourceFileBrowserProps { + label: string; + putHandler: string; + deleteHandler: string; + getHandler: string; + listHandler: string; +} + +export const CustomResourceFileBrowser = ({ + listHandler, + deleteHandler, + putHandler, + getHandler, +}: CustomResourceFileBrowserProps) => { + const { download: downloadFileLocally, downloadFiles: downloadFilesLocally } = + useDownloadFile(); + const { readBlob } = useUploadFile(); + + const [currentFile, setCurrentFile] = useState(); + + const listQuery = trpc["fileBrowser.list"].useQuery( + { + resourcePath: listHandler, + }, + { enabled: !!listHandler }, + ); + + const currentFileContentQuery = trpc["fileBrowser.get"].useQuery( + { + fileName: currentFile ?? "", + resourcePath: getHandler, + }, + { + enabled: currentFile !== undefined, + keepPreviousData: false, + }, + ); + + const deleteMutation = trpc["fileBrowser.delete"].useMutation(); + const downloadMutation = trpc["fileBrowser.download"].useMutation(); + const putMutation = trpc["fileBrowser.put"].useMutation(); + + const [currentFileContent, setCurrentFileContent] = useState(); + + const [files, setFiles] = useState([]); + + useEffect(() => { + if (currentFileContentQuery.data) { + setCurrentFileContent(currentFileContentQuery.data); + } + }, [currentFileContentQuery.data]); + + useEffect(() => { + if (listQuery.data) { + setFiles(listQuery.data); + } + }, [listQuery.data]); + + const deleteFiles = useCallback( + async (files: string[]) => { + for (const file of files) { + await deleteMutation.mutateAsync({ + resourcePath: deleteHandler, + fileName: file, + }); + } + }, + [deleteHandler, deleteMutation], + ); + + const downloadFiles = useCallback( + async (files: string[]) => { + if (files.length === 0) { + return; + } + if (files.length === 1 && files[0]) { + const content = + currentFileContent || + (await downloadMutation.mutate({ + resourcePath: getHandler, + fileName: files[0], + })); + void downloadFileLocally(files[0], content || ""); + } + if (files.length > 1) { + const promises = files.map(async (file) => { + const content = await downloadMutation.mutateAsync({ + resourcePath: getHandler, + fileName: file, + }); + return { filename: file, content }; + }); + + const result = await Promise.all(promises); + void downloadFilesLocally(result); + } + }, + [ + downloadFileLocally, + downloadFilesLocally, + getHandler, + downloadMutation, + currentFileContent, + ], + ); + + const putFile = useCallback( + (fileName: string, fileContent: string) => { + return putMutation.mutateAsync({ + resourcePath: putHandler, + fileName, + fileContent, + }); + }, + [putHandler, putMutation], + ); + + const uploadFiles = useCallback( + async (files: FileList) => { + for (const file of files) { + const fileContent = await readBlob(file.name, file); + void putFile(file.name, fileContent); + } + }, + [putFile, readBlob], + ); + + const isLoading = useMemo(() => { + return currentFileContentQuery.isFetching; + }, [currentFileContentQuery.isFetching]); + + return ( + + ); +}; diff --git a/apps/wing-console/console/ui/src/ui/custom-resource-http-client.tsx b/apps/wing-console/console/ui/src/ui/custom-resource-http-client.tsx new file mode 100644 index 00000000000..9b0119523da --- /dev/null +++ b/apps/wing-console/console/ui/src/ui/custom-resource-http-client.tsx @@ -0,0 +1,62 @@ +import { Attribute } from "@wingconsole/design-system"; +import { useContext, useState } from "react"; + +import { AppContext } from "../AppContext.js"; +import { trpc } from "../services/trpc.js"; +import { useApi } from "../services/use-api.js"; + +import { ApiInteraction } from "./api-interaction.js"; + +export interface CustomResourceHttpClientItemProps { + label: string; + getUrlHandler: string; + getApiSpecHandler: string; +} + +export const CustomResourceHttpClientItem = ({ + label, + getUrlHandler, + getApiSpecHandler, +}: CustomResourceHttpClientItemProps) => { + const { appMode } = useContext(AppContext); + + const urlData = trpc["httpClient.getUrl"].useQuery( + { + resourcePath: getUrlHandler, + }, + { enabled: !!getUrlHandler }, + ); + + const openApiSpecData = trpc["httpClient.getOpenApiSpec"].useQuery( + { + resourcePath: getApiSpecHandler, + }, + { enabled: !!getApiSpecHandler }, + ); + + const [response, setResponse] = useState(); + const { callFetch, isLoading } = useApi({ + onFetchDataUpdate: (data) => { + setResponse(data); + }, + }); + + return ( +
+
+ +
+ {urlData.data?.url && openApiSpecData.data?.openApiSpec && ( + + )} +
+ ); +}; diff --git a/apps/wing-console/console/ui/src/ui/custom-resource-item.tsx b/apps/wing-console/console/ui/src/ui/custom-resource-item.tsx new file mode 100644 index 00000000000..dddeec33179 --- /dev/null +++ b/apps/wing-console/console/ui/src/ui/custom-resource-item.tsx @@ -0,0 +1,71 @@ +import type { + UIComponent, + UIField, + UIButton, + UISection, +} from "@winglang/sdk/lib/core"; + +import type { + UIFileBrowser, + UIHttpClient, +} from "../../../../../../libs/wingsdk/lib/core/index.js"; + +import { CustomResourceFileBrowser } from "./custom-resource-file-browser.js"; +import { CustomResourceHttpClientItem } from "./custom-resource-http-client.js"; +import { CustomResourceUiButtonItem } from "./custom-resource-ui-button.js"; +import { CustomResourceUiFieldItem } from "./custom-resource-ui-field.js"; + +const getUiComponent = (item: UIComponent) => { + if (item.kind === "field") { + return item as UIField; + } + if (item.kind === "button") { + return item as UIButton; + } + if (item.kind === "section") { + return item as UISection; + } + if (item.kind === "file-browser") { + return item as UIFileBrowser; + } + if (item.kind === "http-client") { + return item as UIHttpClient; + } + return item; +}; + +export const CustomResourceUiItem = ({ item }: { item: UIComponent }) => { + const uiComponent = getUiComponent(item); + return ( + <> + {uiComponent.kind === "field" && ( + + )} + {uiComponent.kind === "button" && ( + + )} + {uiComponent.kind === "file-browser" && ( + + )} + {uiComponent.kind === "http-client" && ( + + )} + + ); +}; diff --git a/apps/wing-console/console/ui/src/ui/custom-resource-ui-button.tsx b/apps/wing-console/console/ui/src/ui/custom-resource-ui-button.tsx new file mode 100644 index 00000000000..3bdda793141 --- /dev/null +++ b/apps/wing-console/console/ui/src/ui/custom-resource-ui-button.tsx @@ -0,0 +1,36 @@ +import { useTheme, Button } from "@wingconsole/design-system"; +import classNames from "classnames"; +import { useCallback, useId } from "react"; + +import { trpc } from "../services/trpc.js"; + +export interface CustomResourceUiButtomItemProps { + label: string; + handlerPath: string; +} + +export const CustomResourceUiButtonItem = ({ + label, + handlerPath, +}: CustomResourceUiButtomItemProps) => { + const { theme } = useTheme(); + const { mutate: invokeMutation } = trpc["uiButton.invoke"].useMutation(); + const invoke = useCallback(() => { + invokeMutation({ + resourcePath: handlerPath, + }); + }, [handlerPath, invokeMutation]); + + const id = useId(); + return ( +
+ +
+ ); +}; diff --git a/apps/wing-console/console/ui/src/ui/custom-resource-ui-field.tsx b/apps/wing-console/console/ui/src/ui/custom-resource-ui-field.tsx new file mode 100644 index 00000000000..5534b87910d --- /dev/null +++ b/apps/wing-console/console/ui/src/ui/custom-resource-ui-field.tsx @@ -0,0 +1,21 @@ +import { Attribute } from "@wingconsole/design-system"; + +import { trpc } from "../services/trpc.js"; + +export interface CustomResourceUiFieldItemProps { + label: string; + handlerPath: string; +} + +export const CustomResourceUiFieldItem = ({ + label, + handlerPath, +}: CustomResourceUiFieldItemProps) => { + const field = trpc["uiField.get"].useQuery( + { + resourcePath: handlerPath, + }, + { enabled: !!handlerPath }, + ); + return ; +}; diff --git a/apps/wing-console/console/ui/src/ui/bucket-interaction.tsx b/apps/wing-console/console/ui/src/ui/file-browser.tsx similarity index 93% rename from apps/wing-console/console/ui/src/ui/bucket-interaction.tsx rename to apps/wing-console/console/ui/src/ui/file-browser.tsx index 7afb5ca1511..9574024cf47 100644 --- a/apps/wing-console/console/ui/src/ui/bucket-interaction.tsx +++ b/apps/wing-console/console/ui/src/ui/file-browser.tsx @@ -20,9 +20,9 @@ import { useCallback, useContext, useMemo, useRef, useState } from "react"; import { LayoutContext, LayoutType } from "../layout/layout-provider.js"; -export interface BucketInteractionProps { - selectedEntries: string[]; - entries: TreeEntry[]; +export interface FileBrowserProps { + selectedFiles: string[]; + files: TreeEntry[]; fileInputId: string; selectedFile: string; isLoading: boolean; @@ -35,9 +35,9 @@ export interface BucketInteractionProps { onCurrentEntryChange: (index: string | undefined) => void; } -export const BucketInteraction = ({ - selectedEntries, - entries, +export const FileBrowser = ({ + selectedFiles, + files, fileInputId, selectedFileData, selectedFile, @@ -48,7 +48,7 @@ export const BucketInteraction = ({ onUploadSelectedFilesClick, onSelectedEntriesChange, onCurrentEntryChange, -}: BucketInteractionProps) => { +}: FileBrowserProps) => { const { theme } = useTheme(); const fileInputRef = useRef(null); @@ -82,13 +82,13 @@ export const BucketInteraction = ({
- {entries.length === 0 && ( + {files.length === 0 && (
)} - {entries.length > 0 && ( + {files.length > 0 && ( { - const field = trpc["app.getResourceUiField"].useQuery( - { - resourcePath: handlerPath, - }, - { enabled: !!handlerPath }, - ); - return ; -}; - -interface CustomResourceUiButtomItemProps { - label: string; - handlerPath: string; -} - -const CustomResourceUiButtomItem = ({ - label, - handlerPath, -}: CustomResourceUiButtomItemProps) => { - const { theme } = useTheme(); - const { mutate: invokeMutation } = - trpc["app.invokeResourceUiButton"].useMutation(); - const invoke = useCallback(() => { - invokeMutation({ - resourcePath: handlerPath, - }); - }, [handlerPath, invokeMutation]); - - const id = useId(); - return ( -
- -
- ); -}; - -interface CustomResourceHttpClientItemProps { - label: string; - getUrlHandler: string; - getApiSpecHandler: string; -} - -const CustomResourceHttpClientItem = ({ - label, - getUrlHandler, - getApiSpecHandler, -}: CustomResourceHttpClientItemProps) => { - const { theme } = useTheme(); - const { appMode } = useContext(AppContext); - - const data = trpc["app.getResourceUiHttpClient"].useQuery( - { - getUrlResourcePath: getUrlHandler, - getApiSpecResourcePath: getApiSpecHandler, - }, - { enabled: !!getUrlHandler && !!getApiSpecHandler }, - ); - - const [response, setResponse] = useState(); - const { callFetch, isLoading } = useApi({ - onFetchDataUpdate: (data) => { - setResponse(data); - }, - }); - - return ( -
-
- -
- {data.data?.url && data.data?.openApiSpec && ( - - )} -
- ); -}; - -const getUiComponent = (item: UIComponent) => { - if (item.kind === "field") { - return item as UIField; - } - if (item.kind === "button") { - return item as UIButton; - } - if (item.kind === "section") { - return item as UISection; - } - if (item.kind === "http-client") { - return item as UIHttpClient; - } - return item; -}; - -const CustomResourceUiItem = ({ item }: { item: UIComponent }) => { - const uiComponent = getUiComponent(item); - return ( - <> - {uiComponent.kind === "field" && ( - - )} - {uiComponent.kind === "button" && ( - - )} - {uiComponent.kind === "http-client" && ( - - )} - - ); -}; - interface AttributeGroup { groupName: string; actionName?: string; diff --git a/docs/docs/04-standard-library/ui/api-reference.md b/docs/docs/04-standard-library/ui/api-reference.md index f6f8b77fc4d..011e5b576d2 100644 --- a/docs/docs/04-standard-library/ui/api-reference.md +++ b/docs/docs/04-standard-library/ui/api-reference.md @@ -231,6 +231,112 @@ The tree node. --- +### FileBrowser + +A file browser can be used to browse files. + +#### Initializers + +```wing +bring ui; + +new ui.FileBrowser(label: str, handlers: FileBrowserHandlers); +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| label | str | *No description.* | +| handlers | FileBrowserHandlers | *No description.* | + +--- + +##### `label`Required + +- *Type:* str + +--- + +##### `handlers`Required + +- *Type:* FileBrowserHandlers + +--- + + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| onLiftType | A hook called by the Wing compiler once for each inflight host that needs to use this type inflight. | +| isVisualComponent | Returns whether the given construct is a visual component. | + +--- + +##### `onLiftType` + +```wing +bring ui; + +ui.FileBrowser.onLiftType(host: IInflightHost, ops: MutArray); +``` + +A hook called by the Wing compiler once for each inflight host that needs to use this type inflight. + +The list of requested inflight methods +needed by the inflight host are given by `ops`. + +This method is commonly used for adding permissions, environment variables, or +other capabilities to the inflight host. + +###### `host`Required + +- *Type:* IInflightHost + +--- + +###### `ops`Required + +- *Type:* MutArray<str> + +--- + +##### `isVisualComponent` + +```wing +bring ui; + +ui.FileBrowser.isVisualComponent(c: IConstruct); +``` + +Returns whether the given construct is a visual component. + +###### `c`Required + +- *Type:* constructs.IConstruct + +--- + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| node | constructs.Node | The tree node. | + +--- + +##### `node`Required + +```wing +node: Node; +``` + +- *Type:* constructs.Node + +The tree node. + +--- + + ### HttpClient An HttpClient can be used to make HTTP requests. @@ -769,6 +875,77 @@ How often the field should be refreshed. --- +### FileBrowserHandlers + +File browser handlers. + +#### Initializer + +```wing +bring ui; + +let FileBrowserHandlers = ui.FileBrowserHandlers{ ... }; +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| delete | IFileBrowserDeleteHandler | Handler for deleting a file. | +| get | IFileBrowserGetHandler | Handler for getting a file. | +| list | IFileBrowserListHandler | Handler for listing files. | +| put | IFileBrowserPutHandler | Handler for putting a file. | + +--- + +##### `delete`Required + +```wing +delete: IFileBrowserDeleteHandler; +``` + +- *Type:* IFileBrowserDeleteHandler + +Handler for deleting a file. + +--- + +##### `get`Required + +```wing +get: IFileBrowserGetHandler; +``` + +- *Type:* IFileBrowserGetHandler + +Handler for getting a file. + +--- + +##### `list`Required + +```wing +list: IFileBrowserListHandler; +``` + +- *Type:* IFileBrowserListHandler + +Handler for listing files. + +--- + +##### `put`Required + +```wing +put: IFileBrowserPutHandler; +``` + +- *Type:* IFileBrowserPutHandler + +Handler for putting a file. + +--- + ### SectionProps Props for `Section`. @@ -874,6 +1051,170 @@ inflight handle(): str Function that returns a string to display. +### IFileBrowserDeleteHandler + +- *Extends:* IInflight + +- *Implemented By:* IFileBrowserDeleteHandler + +**Inflight client:** [@winglang/sdk.ui.IFileBrowserDeleteHandlerClient](#@winglang/sdk.ui.IFileBrowserDeleteHandlerClient) + +A resource with an inflight "handle" method that can be passed to `IFileBrowser`. + + + +### IFileBrowserDeleteHandlerClient + +- *Implemented By:* IFileBrowserDeleteHandlerClient + +Inflight client for `IFileBrowserDeleteHandler`. + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| handle | Function that performs an action. | + +--- + +##### `handle` + +```wing +inflight handle(fileName: str): void +``` + +Function that performs an action. + +###### `fileName`Required + +- *Type:* str + +--- + + +### IFileBrowserGetHandler + +- *Extends:* IInflight + +- *Implemented By:* IFileBrowserGetHandler + +**Inflight client:** [@winglang/sdk.ui.IFileBrowserGetHandlerClient](#@winglang/sdk.ui.IFileBrowserGetHandlerClient) + +A resource with an inflight "handle" method that can be passed to `IFileBrowser`. + + + +### IFileBrowserGetHandlerClient + +- *Implemented By:* IFileBrowserGetHandlerClient + +Inflight client for `IFileBrowserGetHandler`. + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| handle | Function that performs an action. | + +--- + +##### `handle` + +```wing +inflight handle(fileName: str): str +``` + +Function that performs an action. + +###### `fileName`Required + +- *Type:* str + +--- + + +### IFileBrowserListHandler + +- *Extends:* IInflight + +- *Implemented By:* IFileBrowserListHandler + +**Inflight client:** [@winglang/sdk.ui.IFileBrowserListHandlerClient](#@winglang/sdk.ui.IFileBrowserListHandlerClient) + +A resource with an inflight "handle" method that can be passed to `IFileBrowser`. + + + +### IFileBrowserListHandlerClient + +- *Implemented By:* IFileBrowserListHandlerClient + +Inflight client for `IFileBrowserListHandler`. + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| handle | Function that performs an action. | + +--- + +##### `handle` + +```wing +inflight handle(): MutArray +``` + +Function that performs an action. + + +### IFileBrowserPutHandler + +- *Extends:* IInflight + +- *Implemented By:* IFileBrowserPutHandler + +**Inflight client:** [@winglang/sdk.ui.IFileBrowserPutHandlerClient](#@winglang/sdk.ui.IFileBrowserPutHandlerClient) + +A resource with an inflight "handle" method that can be passed to `IFileBrowser`. + + + +### IFileBrowserPutHandlerClient + +- *Implemented By:* IFileBrowserPutHandlerClient + +Inflight client for `IFileBrowserVoidHandler`. + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| handle | Function that performs an action. | + +--- + +##### `handle` + +```wing +inflight handle(fileName: str, fileContent: str): void +``` + +Function that performs an action. + +###### `fileName`Required + +- *Type:* str + +--- + +###### `fileContent`Required + +- *Type:* str + +--- + + ### IHttpClientGetApiSpecHandler - *Extends:* IInflight diff --git a/libs/wingsdk/src/core/tree.ts b/libs/wingsdk/src/core/tree.ts index ea08717f302..274db0bdad6 100644 --- a/libs/wingsdk/src/core/tree.ts +++ b/libs/wingsdk/src/core/tree.ts @@ -84,7 +84,12 @@ export interface DisplayInfo { } /** @internal */ -export type UIComponent = UIField | UISection | UIButton | UIHttpClient; +export type UIComponent = + | UIField + | UISection + | UIButton + | UIHttpClient + | UIFileBrowser; /** @internal */ export interface UIField { @@ -119,6 +124,16 @@ export interface UIHttpClient { readonly getApiSpecHandler: string; } +/** @internal */ +export interface UIFileBrowser { + readonly kind: "file-browser"; + readonly label: string; + readonly putHandler: string; + readonly deleteHandler: string; + readonly getHandler: string; + readonly listHandler: string; +} + /** * The construct tree. */ diff --git a/libs/wingsdk/src/ui/file-browser.delete.inflight.ts b/libs/wingsdk/src/ui/file-browser.delete.inflight.ts new file mode 100644 index 00000000000..d03d81a2bbc --- /dev/null +++ b/libs/wingsdk/src/ui/file-browser.delete.inflight.ts @@ -0,0 +1,18 @@ +import { IFileBrowserDeleteHandlerClient } from "./file-browser"; + +export class FileBrowserDeleteHandlerClient + implements IFileBrowserDeleteHandlerClient +{ + private readonly handler: IFileBrowserDeleteHandlerClient; + constructor({ handler }: { handler: IFileBrowserDeleteHandlerClient }) { + this.handler = handler; + } + public async handle(payload: string): Promise { + try { + const fileName = JSON.parse(payload).fileName; + return await this.handler.handle(fileName); + } catch (e) { + throw new Error("Invalid payload for file browser delete handler client"); + } + } +} diff --git a/libs/wingsdk/src/ui/file-browser.get.inflight.ts b/libs/wingsdk/src/ui/file-browser.get.inflight.ts new file mode 100644 index 00000000000..2d075dc667d --- /dev/null +++ b/libs/wingsdk/src/ui/file-browser.get.inflight.ts @@ -0,0 +1,18 @@ +import { IFileBrowserGetHandlerClient } from "./file-browser"; + +export class FileBrowserGetHandlerClient + implements IFileBrowserGetHandlerClient +{ + private readonly handler: IFileBrowserGetHandlerClient; + constructor({ handler }: { handler: IFileBrowserGetHandlerClient }) { + this.handler = handler; + } + public async handle(payload: string): Promise { + try { + const fileName = JSON.parse(payload).fileName; + return await this.handler.handle(fileName); + } catch (e) { + throw new Error("Invalid payload for file browser get handler client"); + } + } +} diff --git a/libs/wingsdk/src/ui/file-browser.list.inflight.ts b/libs/wingsdk/src/ui/file-browser.list.inflight.ts new file mode 100644 index 00000000000..625bebc221b --- /dev/null +++ b/libs/wingsdk/src/ui/file-browser.list.inflight.ts @@ -0,0 +1,17 @@ +import { IFileBrowserListHandlerClient } from "./file-browser"; + +export class FileBrowserListHandlerClient + implements IFileBrowserListHandlerClient +{ + private readonly handler: IFileBrowserListHandlerClient; + constructor({ handler }: { handler: IFileBrowserListHandlerClient }) { + this.handler = handler; + } + public async handle(): Promise { + try { + return await this.handler.handle(); + } catch (e) { + throw new Error("Invalid payload for file browser list handler client"); + } + } +} diff --git a/libs/wingsdk/src/ui/file-browser.put.inflight.ts b/libs/wingsdk/src/ui/file-browser.put.inflight.ts new file mode 100644 index 00000000000..f5dccef3ed3 --- /dev/null +++ b/libs/wingsdk/src/ui/file-browser.put.inflight.ts @@ -0,0 +1,18 @@ +import { IFileBrowserPutHandlerClient } from "./file-browser"; + +export class FileBrowserPutHandlerClient + implements IFileBrowserPutHandlerClient +{ + private readonly handler: IFileBrowserPutHandlerClient; + constructor({ handler }: { handler: IFileBrowserPutHandlerClient }) { + this.handler = handler; + } + public async handle(payload: string): Promise { + try { + const { fileName, fileContent } = JSON.parse(payload); + return await this.handler.handle(fileName, fileContent); + } catch (e) { + throw new Error("Invalid payload for file browser put handler client"); + } + } +} diff --git a/libs/wingsdk/src/ui/file-browser.ts b/libs/wingsdk/src/ui/file-browser.ts new file mode 100644 index 00000000000..2b7184a70c9 --- /dev/null +++ b/libs/wingsdk/src/ui/file-browser.ts @@ -0,0 +1,199 @@ +import { join } from "path"; +import { Construct } from "constructs"; +import { VisualComponent } from "./base"; +import { Function } from "../cloud"; +import { fqnForType } from "../constants"; +import { App, UIComponent } from "../core"; +import { convertBetweenHandlers } from "../shared/convert"; +import { IInflight } from "../std"; + +/** + * Global identifier for `FileBrowser`. + */ +export const FILE_BROWSER_FQN = fqnForType("ui.FileBrowser"); + +/** + * File browser handlers. + */ +export interface FileBrowserHandlers { + /** + * Handler for putting a file. + */ + readonly put: IFileBrowserPutHandler; + /** + * Handler for deleting a file. + */ + readonly delete: IFileBrowserDeleteHandler; + /** + * Handler for getting a file. + */ + readonly get: IFileBrowserGetHandler; + /** + * Handler for listing files. + */ + readonly list: IFileBrowserListHandler; +} + +/** + * A file browser can be used to browse files. + */ +export class FileBrowser extends VisualComponent { + /** + * Creates a new ui.FileBrowser instance through the app. + * @internal + */ + public static _newFileBrowser( + scope: Construct, + id: string, + label: string, + handlers: FileBrowserHandlers + ): FileBrowser { + return App.of(scope).newAbstract( + FILE_BROWSER_FQN, + scope, + id, + label, + handlers + ); + } + + private readonly getFn: Function; + private readonly putFn: Function; + private readonly deleteFn: Function; + private readonly listFn: Function; + private readonly label: string; + + constructor( + scope: Construct, + id: string, + label: string, + handlers: FileBrowserHandlers + ) { + super(scope, id); + this.label = label; + + const getHandler = convertBetweenHandlers( + handlers.get, + join(__dirname, "file-browser.get.inflight.js"), + "FileBrowserGetHandlerClient" + ); + + const putHandler = convertBetweenHandlers( + handlers.put, + join(__dirname, "file-browser.put.inflight.js"), + "FileBrowserPutHandlerClient" + ); + + const deleteHandler = convertBetweenHandlers( + handlers.delete, + join(__dirname, "file-browser.delete.inflight.js"), + "FileBrowserDeleteHandlerClient" + ); + + const listHandler = convertBetweenHandlers( + handlers.list, + join(__dirname, "file-browser.list.inflight.js"), + "FileBrowserListHandlerClient" + ); + + this.getFn = new Function(this, "get", getHandler); + this.putFn = new Function(this, "put", putHandler); + this.deleteFn = new Function(this, "delete", deleteHandler); + this.listFn = new Function(this, "list", listHandler); + } + + /** @internal */ + public _toUIComponent(): UIComponent { + return { + kind: "file-browser", + label: this.label, + putHandler: this.putFn.node.path, + deleteHandler: this.deleteFn.node.path, + getHandler: this.getFn.node.path, + listHandler: this.listFn.node.path, + }; + } + + /** @internal */ + public _supportedOps(): string[] { + return []; + } + + /** @internal */ + public _toInflight(): string { + throw new Error("Method not implemented."); + } +} + +/** + * A resource with an inflight "handle" method that can be passed to + * `IFileBrowser`. + * + * @inflight `@winglang/sdk.ui.IFileBrowserPutHandlerClient` + */ +export interface IFileBrowserPutHandler extends IInflight {} +/** + * A resource with an inflight "handle" method that can be passed to + * `IFileBrowser`. + * + * @inflight `@winglang/sdk.ui.IFileBrowserGetHandlerClient` + */ +export interface IFileBrowserGetHandler extends IInflight {} +/** + * A resource with an inflight "handle" method that can be passed to + * `IFileBrowser`. + * + * @inflight `@winglang/sdk.ui.IFileBrowserListHandlerClient` + */ +export interface IFileBrowserListHandler extends IInflight {} +/** + * A resource with an inflight "handle" method that can be passed to + * `IFileBrowser`. + * + * @inflight `@winglang/sdk.ui.IFileBrowserDeleteHandlerClient` + */ +export interface IFileBrowserDeleteHandler extends IInflight {} + +/** + * Inflight client for `IFileBrowserVoidHandler`. + */ +export interface IFileBrowserPutHandlerClient { + /** + * Function that performs an action. + * @inflight + */ + handle(fileName: string, fileContent: string): Promise; +} + +/** + * Inflight client for `IFileBrowserGetHandler`. + */ +export interface IFileBrowserGetHandlerClient { + /** + * Function that performs an action. + * @inflight + */ + handle(fileName: string): Promise; +} + +/** + * Inflight client for `IFileBrowserDeleteHandler`. + */ +export interface IFileBrowserDeleteHandlerClient { + /** + * Function that performs an action. + * @inflight + */ + handle(fileName: string): Promise; +} + +/** + * Inflight client for `IFileBrowserListHandler`. + */ +export interface IFileBrowserListHandlerClient { + /** + * Function that performs an action. + * @inflight + */ + handle(): Promise; +} diff --git a/libs/wingsdk/src/ui/index.ts b/libs/wingsdk/src/ui/index.ts index aef2d432563..559dec6685f 100644 --- a/libs/wingsdk/src/ui/index.ts +++ b/libs/wingsdk/src/ui/index.ts @@ -2,5 +2,6 @@ export * from "./base"; export * from "./button"; export * from "./colors"; export * from "./field"; +export * from "./file-browser"; export * from "./http-client"; export * from "./section";