Openstatus www.openstatus.dev
at 20d0eeac16db94063a196dbfa8cb02fb172cac98 356 lines 12 kB view raw
1"use client"; 2import { useFieldArray } from "react-hook-form"; 3import type { UseFormReturn } from "react-hook-form"; 4 5import { 6 numberCompareDictionary, 7 stringCompareDictionary, 8} from "@openstatus/assertions"; 9import type { InsertMonitor } from "@openstatus/db/src/schema"; 10import { 11 Button, 12 FormControl, 13 FormDescription, 14 FormField, 15 FormItem, 16 FormLabel, 17 Input, 18 Select, 19 SelectContent, 20 SelectItem, 21 SelectTrigger, 22 SelectValue, 23} from "@openstatus/ui"; 24 25import { EmptyState } from "@/components/dashboard/empty-state"; 26import { Icons } from "@/components/icons"; 27import { SectionHeader } from "../shared/section-header"; 28 29// IMPROVEMENT: use FormFields incl. error message (fixes the Select component) 30 31export const setEmptyOrStr = (v: unknown) => { 32 if (typeof v === "string" && v.trim() === "") return undefined; 33 return v; 34}; 35 36interface Props { 37 form: UseFormReturn<InsertMonitor>; 38} 39 40// REMINDER: once we have different types of assertions based on different jobTypes 41// we shoulds start creating a mapping function with allowed assertions for each jobType 42 43export function SectionAssertions({ form }: Props) { 44 const statusAssertions = useFieldArray({ 45 control: form.control, 46 name: "statusAssertions", 47 }); 48 const headerAssertions = useFieldArray({ 49 control: form.control, 50 name: "headerAssertions", 51 }); 52 const textBodyAssertions = useFieldArray({ 53 control: form.control, 54 name: "textBodyAssertions", 55 }); 56 57 return ( 58 <div className="grid w-full gap-4"> 59 <SectionHeader 60 title="Timing Setting" 61 description={ 62 <> 63 Add specific time limits to your requests to receive notifications 64 if an endpoint takes longer than expected. 65 </> 66 } 67 /> 68 <div className="grid w-full gap-4 sm:grid-cols-6"> 69 <FormField 70 control={form.control} 71 name="degradedAfter" 72 render={({ field }) => ( 73 <FormItem className="col-span-6 sm:col-span-3"> 74 <FormLabel> 75 Degraded <span className="font-normal">(in ms.)</span> 76 </FormLabel> 77 <FormControl> 78 <Input 79 type="number" 80 min={0} 81 max={60000} 82 placeholder="30000" 83 {...form.register(field.name, { 84 setValueAs: (v) => (v === "" ? null : v), 85 })} 86 /> 87 </FormControl> 88 <FormDescription> 89 Time after which the endpoint is considered degraded. 90 </FormDescription> 91 </FormItem> 92 )} 93 /> 94 <FormField 95 control={form.control} 96 name="timeout" 97 render={({ field }) => ( 98 <FormItem className="col-span-6 sm:col-span-3"> 99 <FormLabel> 100 Timeout <span className="font-normal">(in ms.)</span> 101 </FormLabel> 102 <FormControl> 103 <Input 104 type="number" 105 placeholder="45000" 106 min={0} 107 max={60000} 108 {...field} 109 /> 110 </FormControl> 111 <FormDescription> 112 Max. time allowed for request to complete. 113 </FormDescription> 114 115 {/* <FormMessage /> */} 116 </FormItem> 117 )} 118 /> 119 </div> 120 <SectionHeader 121 title="Assertions" 122 description={ 123 <> 124 Validate the response to ensure your service is working as expected. 125 <br /> 126 <span className="underline decoration-border underline-offset-4"> 127 By default, we check for a{" "} 128 <span className="font-medium text-foreground"> 129 <code>2xx</code> status code 130 </span> 131 </span> 132 . 133 </> 134 } 135 /> 136 {form.getValues("jobType") === "http" ? ( 137 <div className="flex flex-col gap-4"> 138 {statusAssertions.fields.map((f, i) => ( 139 <div key={f.id} className="grid grid-cols-12 items-center gap-4"> 140 <p className="col-span-2 text-muted-foreground text-sm"> 141 Status Code 142 </p> 143 <div className="col-span-3" /> 144 <FormField 145 control={form.control} 146 name={`statusAssertions.${i}.compare`} 147 render={({ field }) => ( 148 <FormItem className="col-span-3 w-full"> 149 <Select 150 onValueChange={field.onChange} 151 defaultValue={field.value} 152 > 153 <FormControl> 154 <SelectTrigger> 155 <SelectValue defaultValue="eq" placeholder="Equal" /> 156 </SelectTrigger> 157 </FormControl> 158 <SelectContent> 159 {Object.entries(numberCompareDictionary).map( 160 ([key, value]) => ( 161 <SelectItem key={key} value={key}> 162 {value} 163 </SelectItem> 164 ), 165 )} 166 </SelectContent> 167 </Select> 168 </FormItem> 169 )} 170 /> 171 <Input 172 {...form.register(`statusAssertions.${i}.target`, { 173 required: true, 174 valueAsNumber: true, 175 validate: (value) => 176 value <= 599 || "Value must be 599 or lower", 177 })} 178 type="number" 179 placeholder="200" 180 className="col-span-3" 181 /> 182 <div className="col-span-1"> 183 <Button 184 size="icon" 185 onClick={() => statusAssertions.remove(i)} 186 variant="ghost" 187 type="button" 188 > 189 <Icons.trash className="h-4 w-4" /> 190 </Button> 191 </div> 192 </div> 193 ))} 194 {headerAssertions.fields.map((f, i) => ( 195 <div key={f.id} className="grid grid-cols-12 items-center gap-4"> 196 <p className="col-span-2 text-muted-foreground text-sm"> 197 Response Header 198 </p> 199 <Input 200 {...form.register(`headerAssertions.${i}.key`, { 201 required: true, 202 setValueAs: setEmptyOrStr, 203 })} 204 className="col-span-3" 205 placeholder="X-Header" 206 /> 207 <FormField 208 control={form.control} 209 name={`headerAssertions.${i}.compare`} 210 render={({ field }) => ( 211 <FormItem className="col-span-3 w-full"> 212 <Select 213 onValueChange={field.onChange} 214 defaultValue={field.value} 215 > 216 <FormControl> 217 <SelectTrigger> 218 <SelectValue defaultValue="eq" placeholder="Equal" /> 219 </SelectTrigger> 220 </FormControl> 221 <SelectContent> 222 {Object.entries(stringCompareDictionary).map( 223 ([key, value]) => ( 224 <SelectItem key={key} value={key}> 225 {value} 226 </SelectItem> 227 ), 228 )} 229 </SelectContent> 230 </Select> 231 </FormItem> 232 )} 233 /> 234 <Input 235 {...form.register(`headerAssertions.${i}.target`)} 236 className="col-span-3" 237 placeholder="x-value" 238 /> 239 <div className="col-span-1"> 240 <Button 241 size="icon" 242 onClick={() => headerAssertions.remove(i)} 243 variant="ghost" 244 > 245 <Icons.trash className="h-4 w-4" /> 246 </Button> 247 </div> 248 </div> 249 ))} 250 {textBodyAssertions.fields.map((f, i) => ( 251 <div key={f.id} className="grid grid-cols-12 items-center gap-4"> 252 <p className="col-span-2 text-muted-foreground text-sm">Body</p> 253 <div className="col-span-3" /> 254 <FormField 255 control={form.control} 256 name={`textBodyAssertions.${i}.compare`} 257 render={({ field }) => ( 258 <FormItem className="col-span-3 w-full"> 259 <Select 260 onValueChange={field.onChange} 261 defaultValue={field.value} 262 > 263 <FormControl> 264 <SelectTrigger> 265 <SelectValue defaultValue="eq" placeholder="Equal" /> 266 </SelectTrigger> 267 </FormControl> 268 <SelectContent> 269 {Object.entries(stringCompareDictionary).map( 270 ([key, value]) => ( 271 <SelectItem key={key} value={key}> 272 {value} 273 </SelectItem> 274 ), 275 )} 276 </SelectContent> 277 </Select> 278 </FormItem> 279 )} 280 /> 281 <Input 282 {...form.register(`textBodyAssertions.${i}.target`, { 283 required: true, 284 })} 285 placeholder="<html>...</html>" 286 className="col-span-3" 287 /> 288 <div className="col-span-1"> 289 <Button 290 size="icon" 291 onClick={() => textBodyAssertions.remove(i)} 292 variant="ghost" 293 type="button" 294 > 295 <Icons.trash className="h-4 w-4" /> 296 </Button> 297 </div> 298 </div> 299 ))} 300 <div className="flex flex-wrap gap-4"> 301 <Button 302 variant="outline" 303 type="button" 304 onClick={() => 305 statusAssertions.append({ 306 version: "v1", 307 type: "status", 308 compare: "eq", 309 target: 200, 310 }) 311 } 312 > 313 Add Status Code Assertion 314 </Button> 315 <Button 316 variant="outline" 317 type="button" 318 onClick={() => 319 headerAssertions.append({ 320 version: "v1", 321 type: "header", 322 key: "Content-Type", 323 compare: "eq", 324 target: "application/json", 325 }) 326 } 327 > 328 Add Header Assertion 329 </Button> 330 331 <Button 332 variant="outline" 333 type="button" 334 onClick={() => 335 textBodyAssertions.append({ 336 version: "v1", 337 type: "textBody", 338 compare: "eq", 339 target: "", 340 }) 341 } 342 > 343 Add String Body Assertion 344 </Button> 345 </div> 346 </div> 347 ) : ( 348 <EmptyState 349 icon="alert-triangle" 350 title="No Assertions" 351 description="Assertions are only available for HTTP monitors." 352 /> 353 )} 354 </div> 355 ); 356}