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