open, interoperable sandbox platform for agents and humans 📦 ✨ pocketenv.io
claude-code atproto sandbox openclaw agent
at main 233 lines 7.3 kB view raw
1import { useEffect, useState } from "react"; 2import { createPortal } from "react-dom"; 3import { useForm } from "react-hook-form"; 4import { z } from "zod"; 5import { zodResolver } from "@hookform/resolvers/zod"; 6import { 7 useAddVolumeMutation, 8 useUpdateVolumeMutation, 9 useVolumeQuery, 10} from "../../../hooks/useVolume"; 11import { useNotyf } from "../../../hooks/useNotyf"; 12 13const schema = z.object({ 14 name: z.string().min(1, "Name is required"), 15 path: z.string().min(1, "Path is required"), 16}); 17 18type FormValues = z.infer<typeof schema>; 19 20export type AddVolumeModalProps = { 21 isOpen: boolean; 22 onClose: () => void; 23 sandboxId: string; 24 volumeId?: string; 25}; 26 27function AddVolumeModal({ 28 isOpen, 29 onClose, 30 sandboxId, 31 volumeId, 32}: AddVolumeModalProps) { 33 const notyf = useNotyf(); 34 const [isLoading, setIsLoading] = useState(false); 35 const { mutateAsync: addVolume } = useAddVolumeMutation(); 36 const { mutateAsync: updateVolume } = useUpdateVolumeMutation(); 37 const { data } = useVolumeQuery(volumeId!); 38 const { 39 register, 40 handleSubmit, 41 reset, 42 setValue, 43 formState: { errors }, 44 } = useForm<FormValues>({ 45 resolver: zodResolver(schema), 46 }); 47 48 useEffect(() => { 49 if (data) { 50 setValue("name", data.volume.name); 51 setValue("path", data.volume.path); 52 } 53 }, [data, setValue]); 54 55 useEffect(() => { 56 const handleEscapeKey = (event: KeyboardEvent) => { 57 if (event.key === "Escape" && isOpen) { 58 reset(); 59 onClose(); 60 } 61 }; 62 63 document.addEventListener("keydown", handleEscapeKey); 64 return () => { 65 document.removeEventListener("keydown", handleEscapeKey); 66 }; 67 }, [isOpen, onClose, reset]); 68 69 const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => { 70 e.stopPropagation(); 71 if (e.target === e.currentTarget) { 72 reset(); 73 onClose(); 74 } 75 }; 76 77 const handleContentClick = (e: React.MouseEvent<HTMLDivElement>) => { 78 e.stopPropagation(); 79 }; 80 81 const handleCloseButton = (e: React.MouseEvent<HTMLButtonElement>) => { 82 e.stopPropagation(); 83 reset(); 84 onClose(); 85 }; 86 87 const onSubmit = async (data: FormValues) => { 88 setIsLoading(true); 89 try { 90 if (volumeId) { 91 await updateVolume({ 92 id: volumeId, 93 name: data.name, 94 path: data.path, 95 sandboxId, 96 }); 97 setIsLoading(false); 98 reset(); 99 onClose(); 100 notyf.open("primary", "Volume updated successfully!"); 101 return; 102 } 103 await addVolume({ 104 sandboxId, 105 name: data.name, 106 path: data.path, 107 }); 108 setIsLoading(false); 109 reset(); 110 onClose(); 111 notyf.open("primary", "Volume added successfully"); 112 } catch { 113 setIsLoading(false); 114 reset(); 115 onClose(); 116 notyf.open("error", "Failed to add volume"); 117 } 118 }; 119 120 if (!isOpen) return null; 121 122 return createPortal( 123 <> 124 <div 125 className="overlay modal modal-middle overlay-open:opacity-100 overlay-open:duration-300 open opened" 126 role="dialog" 127 style={{ outline: "none", zIndex: 80 }} 128 onClick={handleBackdropClick} 129 onMouseDown={handleBackdropClick} 130 > 131 <div 132 className={`overlay-animation-target modal-dialog overlay-open:duration-300 transition-all ease-out modal-dialog-lg overlay-open:mt-4 mt-12`} 133 onClick={handleContentClick} 134 onMouseDown={handleContentClick} 135 > 136 <div className="modal-content"> 137 <div className="modal-header"> 138 <div className="flex-1"> 139 {volumeId ? "Edit Volume" : "Add Volume"} 140 </div> 141 <button 142 type="button" 143 className="btn btn-text btn-circle btn-sm absolute end-3 top-3" 144 aria-label="Close" 145 onClick={handleCloseButton} 146 onMouseDown={(e) => e.stopPropagation()} 147 > 148 <span className="icon-[tabler--x] size-4"></span> 149 </button> 150 </div> 151 <form onSubmit={handleSubmit(onSubmit)}> 152 <div className="modal-body" style={{ height: 300 }}> 153 <div className="form-control w-full"> 154 <label className="label"> 155 <span className="label-text font-bold mb-1 text-[14px]"> 156 Name 157 </span> 158 </label> 159 <div 160 className={`input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent`} 161 > 162 <input 163 type="text" 164 placeholder="Volume Name" 165 className={`grow`} 166 autoComplete="off" 167 data-1p-ignore 168 data-lpignore="true" 169 data-form-type="other" 170 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 171 {...register("name")} 172 /> 173 </div> 174 {errors.name && ( 175 <span className="text-error text-[12px] mt-1"> 176 {errors.name.message} 177 </span> 178 )} 179 <div className="mt-5"> 180 <label className="label"> 181 <span className="label-text font-bold mb-1 text-[14px]"> 182 Path 183 </span> 184 </label> 185 <div 186 className={`input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent`} 187 > 188 <input 189 type="text" 190 placeholder="Mount Path, e.g /data" 191 className={`grow`} 192 autoComplete="off" 193 data-1p-ignore 194 data-lpignore="true" 195 data-form-type="other" 196 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 197 {...register("path")} 198 /> 199 </div> 200 {errors.path && ( 201 <span className="text-error text-[12px] mt-1 block"> 202 {errors.path.message} 203 </span> 204 )} 205 </div> 206 </div> 207 </div> 208 <div className="modal-footer"> 209 <button type="submit" className="btn btn-primary font-semibold"> 210 {isLoading && ( 211 <span className="loading loading-spinner loading-xs mr-1.5"></span> 212 )} 213 {volumeId ? "Save Changes" : "Add Volume"} 214 </button> 215 </div> 216 </form> 217 </div> 218 </div> 219 </div> 220 221 <div 222 data-overlay-backdrop-template="" 223 style={{ zIndex: 79 }} 224 className="overlay-backdrop transition duration-300 fixed inset-0 bg-base-300/60 overflow-y-auto opacity-75" 225 onClick={handleBackdropClick} 226 onMouseDown={(e) => e.stopPropagation()} 227 ></div> 228 </>, 229 document.body, 230 ); 231} 232 233export default AddVolumeModal;