diff --git a/web/src/workspaces/api.ts b/web/src/workspaces/api.ts index 46b568e..4bb1ef5 100644 --- a/web/src/workspaces/api.ts +++ b/web/src/workspaces/api.ts @@ -1,7 +1,7 @@ import { fetchApi, type ApiError } from "@/api"; import type { QueryStatus } from "@/lib/query"; import { useCallback, useState } from "react"; -import useSWR, { useSWRConfig } from "swr"; +import useSWR, { mutate, useSWRConfig } from "swr"; import { type Workspace, type WorkspacePortMapping, @@ -200,6 +200,46 @@ function useAddWorkspacePort() { return { addWorkspacePort, status }; } +function useDeleteWorkspacePort() { + const [status, setStatus] = useState>({ type: "idle" }); + + const deleteWorkspacePort = useCallback( + async (workspaceName: string, portName: string) => { + setStatus({ type: "loading" }); + try { + await mutate( + "/workspaces", + fetchApi(`/workspaces/${workspaceName}/forwarded-ports/${portName}`, { + method: "DELETE", + }), + { + populateCache: (_, workspaces) => + workspaces.map( + (it: Workspace): Workspace => + it.name === workspaceName + ? { + ...it, + ports: it.ports?.filter( + (port) => port.subdomain !== portName, + ), + } + : it, + ), + revalidate: false, + throwOnError: true, + }, + ); + setStatus({ type: "ok" }); + } catch (error: unknown) { + setStatus({ type: "error", error: error as ApiError }); + } + }, + [], + ); + + return { deleteWorkspacePort, status }; +} + function useWorkspaceRuntimes() { return useSWR( "/workspace-runtimes", @@ -215,4 +255,5 @@ export { useDeleteWorkspace, useAddWorkspacePort, useWorkspaceRuntimes, + useDeleteWorkspacePort, }; diff --git a/web/src/workspaces/workspace-port-info-tab.tsx b/web/src/workspaces/workspace-port-info-tab.tsx index d85f485..3273ed1 100644 --- a/web/src/workspaces/workspace-port-info-tab.tsx +++ b/web/src/workspaces/workspace-port-info-tab.tsx @@ -10,13 +10,14 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { useToast } from "@/hooks/use-toast"; import { superstructResolver } from "@hookform/resolvers/superstruct"; import { Check, Trash2, X } from "lucide-react"; -import { useContext, useId } from "react"; +import { useContext, useEffect, useId } from "react"; import { useForm } from "react-hook-form"; import { type Infer, number, object, pattern, size, string } from "superstruct"; import { create } from "zustand"; -import { useAddWorkspacePort } from "./api"; +import { useAddWorkspacePort, useDeleteWorkspacePort } from "./api"; import { WorkspaceTableRowContext } from "./workspace-table"; interface PortInfoTabStore { @@ -57,7 +58,7 @@ function PortInfoTableBody() { {subdomain} {port} - + ))} @@ -183,10 +184,34 @@ function NewPortMappingRow() { ); } -function DeletePortMappingButton() { +function DeletePortMappingButton({ subdomain }: { subdomain: string }) { + const { deleteWorkspacePort, status } = useDeleteWorkspacePort(); + const { toast } = useToast(); + const workspace = useContext(WorkspaceTableRowContext); + const isDeleting = status.type === "loading"; + + useEffect(() => { + if (status.type === "error") { + toast({ + variant: "destructive", + title: "Failed to delete port.", + description: "Unexpected error.", + }); + } + }, [status.type, toast]); + + async function _deleteWorkspacePort() { + await deleteWorkspacePort(workspace.name, subdomain); + } + return ( - ); }