Openstatus
www.openstatus.dev
1"use client";
2
3import { Send } from "lucide-react";
4import React from "react";
5import type { UseFormReturn } from "react-hook-form";
6
7import { deserialize } from "@openstatus/assertions";
8import type { InsertMonitor } from "@openstatus/db/src/schema";
9import {
10 type Region,
11 monitorRegions,
12} from "@openstatus/db/src/schema/constants";
13import { regionDict } from "@openstatus/regions";
14import {
15 Button,
16 Dialog,
17 DialogContent,
18 DialogHeader,
19 DialogTitle,
20 Select,
21 SelectContent,
22 SelectItem,
23 SelectTrigger,
24 SelectValue,
25 Tooltip,
26 TooltipContent,
27 TooltipProvider,
28 TooltipTrigger,
29} from "@openstatus/ui";
30
31import { LoadingAnimation } from "@/components/loading-animation";
32import { RegionInfo } from "@/components/ping-response-analysis/region-info";
33import { ResponseDetailTabs } from "@/components/ping-response-analysis/response-detail-tabs";
34import type { RegionChecker } from "@/components/ping-response-analysis/utils";
35import { toast, toastAction } from "@/lib/toast";
36import type { Limits } from "@openstatus/db/src/schema/plan/schema";
37import { getLimit } from "@openstatus/db/src/schema/plan/utils";
38
39interface Props {
40 form: UseFormReturn<InsertMonitor>;
41 limits: Limits;
42 pingEndpoint(
43 region?: Region,
44 ): Promise<{ data?: RegionChecker; error?: string }>;
45}
46
47export function RequestTestButton({ form, pingEndpoint, limits }: Props) {
48 const [check, setCheck] = React.useState<
49 { data: RegionChecker; error?: string } | undefined
50 >();
51 const [value, setValue] = React.useState<Region>(monitorRegions[0]);
52 const [isPending, startTransition] = React.useTransition();
53
54 const onClick = () => {
55 if (isPending) return;
56
57 const { url } = form.getValues();
58
59 if (!url) {
60 toastAction("test-warning-empty-url");
61 return;
62 }
63
64 startTransition(async () => {
65 try {
66 const { data, error } = await pingEndpoint(value);
67 if (data) setCheck({ data, error });
68 const isOk = !error;
69 if (isOk) {
70 toastAction("test-success");
71 } else {
72 toast.error(error);
73 }
74 } catch {
75 toastAction("error");
76 }
77 });
78 };
79
80 const { flag } = regionDict[value as keyof typeof regionDict];
81
82 const { statusAssertions, headerAssertions } = form.getValues();
83
84 const regions = getLimit(limits, "regions");
85
86 return (
87 <Dialog open={!!check} onOpenChange={() => setCheck(undefined)}>
88 <div className="group flex h-10 items-center rounded-md bg-transparent text-sm ring-offset-background focus-within:outline-hidden focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
89 <Select
90 value={value}
91 onValueChange={(value: Region) => setValue(value)}
92 >
93 <SelectTrigger
94 className="flex-1 rounded-r-none border-accent focus:ring-0"
95 aria-label={value}
96 >
97 <SelectValue>{flag}</SelectValue>
98 </SelectTrigger>
99 <SelectContent>
100 {regions.map((region) => {
101 const { flag } = regionDict[region];
102 return (
103 <SelectItem key={region} value={region}>
104 {flag} <span className="ml-1 font-mono">{region}</span>
105 </SelectItem>
106 );
107 })}
108 </SelectContent>
109 </Select>
110 <TooltipProvider>
111 <Tooltip>
112 <TooltipTrigger asChild>
113 <Button
114 onClick={onClick}
115 disabled={isPending}
116 className="h-full flex-1 rounded-l-none focus:ring-0"
117 variant="secondary"
118 >
119 {isPending ? (
120 <LoadingAnimation variant="inverse" />
121 ) : (
122 <>
123 Test <Send className="ml-2 h-4 w-4" />
124 </>
125 )}
126 </Button>
127 </TooltipTrigger>
128 <TooltipContent>
129 <p>Ping your endpoint</p>
130 </TooltipContent>
131 </Tooltip>
132 </TooltipProvider>
133 </div>
134 <DialogContent className="max-h-screen w-full overflow-auto sm:max-w-3xl sm:p-8">
135 <DialogHeader>
136 <DialogTitle>Response</DialogTitle>
137 </DialogHeader>
138 {check?.data.state === "success" ? (
139 <div className="grid gap-8">
140 <RegionInfo check={check.data} error={check.error} />
141 {check.data.state === "success" && check.data.type === "http" ? (
142 <ResponseDetailTabs
143 timing={check.data.timing}
144 headers={check.data.headers}
145 status={check.data.status}
146 assertions={deserialize(
147 JSON.stringify([
148 ...(statusAssertions || []),
149 ...(headerAssertions || []),
150 ]),
151 )}
152 />
153 ) : null}
154 </div>
155 ) : null}
156 </DialogContent>
157 </Dialog>
158 );
159}