Openstatus www.openstatus.dev

Show confirmation modal when saving a monitor with a failed ping (#336)

* show confirmation modal when link ping fails

* failed ping confirmation modal

* remove ternary operator for showing confirm modal

* use useTransition to display loader

* remove unused prop from type definition

* remove unused prop

* update prop name

* rename `handleDataInsertion` prop to `onConfirm`

* wait for save to finish before closing modal

* chore: add automatic check description to form

---------

Co-authored-by: mxkaske <maximilian@kaske.org>

authored by

Kelvin Amoaba
mxkaske
and committed by
GitHub
50cf285f 4fb04a6c

+137 -47
+74 -47
apps/web/src/components/forms/montitor-form.tsx
··· 56 56 } from "@openstatus/ui"; 57 57 58 58 import { LoadingAnimation } from "@/components/loading-animation"; 59 + import { FailedPingAlertConfirmation } from "@/components/modals/failed-ping-alert-confirmation"; 59 60 import { regionsDict } from "@/data/regions-dictionary"; 60 61 import { useToastAction } from "@/hooks/use-toast-action"; 61 62 import useUpdateSearchParams from "@/hooks/use-update-search-params"; ··· 86 87 87 88 const mergedSchema = insertMonitorSchema.merge(advancedSchema); 88 89 89 - type MonitorProps = z.infer<typeof mergedSchema>; 90 + export type MonitorProps = z.infer<typeof mergedSchema>; 90 91 91 92 interface Props { 92 93 defaultValues?: MonitorProps; ··· 122 123 const router = useRouter(); 123 124 const [isPending, startTransition] = React.useTransition(); 124 125 const [isTestPending, startTestTransition] = React.useTransition(); 126 + const [pingFailed, setPingFailed] = React.useState(false); 125 127 const [openDialog, setOpenDialog] = React.useState(false); 126 128 const { toast } = useToastAction(); 127 129 const watchMethod = form.watch("method"); ··· 132 134 control: form.control, 133 135 }); 134 136 137 + const handleDataUpdateOrInsertion = async (props: MonitorProps) => { 138 + try { 139 + if (defaultValues) { 140 + await api.monitor.updateMonitor.mutate(props); 141 + } else { 142 + const monitor = await api.monitor.createMonitor.mutate({ 143 + data: props, 144 + workspaceSlug, 145 + }); 146 + const id = monitor?.id || null; 147 + router.replace(`?${updateSearchParams({ id })}`); 148 + } 149 + router.refresh(); 150 + toast("saved"); 151 + } catch (error) { 152 + toast("error"); 153 + } 154 + }; 155 + 135 156 const onSubmit = ({ ...props }: MonitorProps) => { 136 157 startTransition(async () => { 137 - try { 138 - // TODO: we could use an upsertPage function instead - insert if not exist otherwise update 139 - if (defaultValues) { 140 - await api.monitor.updateMonitor.mutate(props); 141 - } else { 142 - const monitor = await api.monitor.createMonitor.mutate({ 143 - data: props, 144 - workspaceSlug, 145 - }); 146 - const id = monitor?.id || null; 147 - router.replace(`?${updateSearchParams({ id })}`); 148 - } 149 - router.refresh(); 150 - toast("saved"); 151 - } catch { 152 - toast("error"); 158 + const pingResult = await pingEndpoint(); 159 + if (!pingResult) { 160 + setPingFailed(true); 161 + return; 153 162 } 163 + await handleDataUpdateOrInsertion(props); 154 164 }); 155 165 }; 156 166 ··· 177 187 } 178 188 }; 179 189 190 + const pingEndpoint = async () => { 191 + const { url, body, method, headers } = form.getValues(); 192 + const res = await fetch(`/api/checker/test`, { 193 + method: "POST", 194 + headers: new Headers({ 195 + "Content-Type": "application/json", 196 + }), 197 + body: JSON.stringify({ url, body, method, headers }), 198 + }); 199 + return res.ok; 200 + }; 201 + 180 202 const sendTestPing = () => { 181 203 startTestTransition(async () => { 182 - const res = await fetch(`/api/checker/test`, { 183 - method: "POST", 184 - headers: new Headers({ 185 - "Content-Type": "application/json", 186 - }), 187 - body: JSON.stringify({ 188 - url: form.getValues("url"), 189 - body: form.getValues("body"), 190 - method: form.getValues("method"), 191 - headers: form.getValues("headers"), 192 - }), 193 - }); 194 - if (res.ok) { 204 + const isSuccessful = await pingEndpoint(); 205 + if (isSuccessful) { 195 206 toast("test-success"); 196 207 } else { 197 208 toast("test-error"); ··· 650 661 </AccordionContent> 651 662 </AccordionItem> 652 663 </Accordion> 653 - <div className="flex flex-col gap-6 sm:col-span-full sm:flex-row sm:justify-end"> 654 - <Button 655 - type="button" 656 - variant="secondary" 657 - className="w-full sm:w-auto" 658 - size="lg" 659 - onClick={sendTestPing} 660 - > 661 - {!isTestPending ? ( 662 - "Test Request" 663 - ) : ( 664 - <LoadingAnimation variant="inverse" /> 665 - )} 666 - </Button> 667 - <Button className="w-full sm:w-auto" size="lg" disabled={isPending}> 668 - {!isPending ? "Confirm" : <LoadingAnimation />} 669 - </Button> 664 + <div className="grid justify-end gap-3"> 665 + <div className="flex flex-col gap-6 sm:col-span-full sm:flex-row sm:justify-end"> 666 + <Button 667 + type="button" 668 + variant="secondary" 669 + className="w-full sm:w-auto" 670 + size="lg" 671 + onClick={sendTestPing} 672 + > 673 + {!isTestPending ? ( 674 + "Test Request" 675 + ) : ( 676 + <LoadingAnimation variant="inverse" /> 677 + )} 678 + </Button> 679 + <Button 680 + className="w-full sm:w-auto" 681 + size="lg" 682 + disabled={isPending} 683 + > 684 + {!isPending ? "Confirm" : <LoadingAnimation />} 685 + </Button> 686 + </div> 687 + <div className="flex w-full justify-end"> 688 + <p className="text-muted-foreground text-xs"> 689 + We test your endpoint connection on submit. 690 + </p> 691 + </div> 670 692 </div> 671 693 </form> 672 694 </Form> ··· 682 704 {...{ workspaceSlug }} 683 705 /> 684 706 </DialogContent> 707 + <FailedPingAlertConfirmation 708 + monitor={form.getValues()} 709 + {...{ pingFailed, setPingFailed }} 710 + onConfirm={handleDataUpdateOrInsertion} 711 + /> 685 712 </Dialog> 686 713 ); 687 714 }
+63
apps/web/src/components/modals/failed-ping-alert-confirmation.tsx
··· 1 + import React from "react"; 2 + 3 + import { 4 + AlertDialog, 5 + AlertDialogAction, 6 + AlertDialogCancel, 7 + AlertDialogContent, 8 + AlertDialogDescription, 9 + AlertDialogFooter, 10 + AlertDialogHeader, 11 + AlertDialogTitle, 12 + } from "@openstatus/ui"; 13 + 14 + import { LoadingAnimation } from "@/components/loading-animation"; 15 + import type { MonitorProps } from "../forms/montitor-form"; 16 + 17 + type FailedPingAlertConfirmationProps = { 18 + monitor: MonitorProps; 19 + pingFailed: boolean; 20 + setPingFailed: React.Dispatch<React.SetStateAction<boolean>>; 21 + onConfirm: (props: MonitorProps) => Promise<void>; 22 + }; 23 + 24 + export const FailedPingAlertConfirmation = ({ 25 + onConfirm: upsertMonitor, 26 + pingFailed, 27 + setPingFailed, 28 + monitor, 29 + }: FailedPingAlertConfirmationProps) => { 30 + const [isPending, startTransition] = React.useTransition(); 31 + const handleSubmit = () => { 32 + startTransition(async () => { 33 + await upsertMonitor(monitor); 34 + setPingFailed(false); 35 + }); 36 + }; 37 + 38 + return ( 39 + <AlertDialog open={pingFailed} onOpenChange={setPingFailed}> 40 + <AlertDialogContent> 41 + <AlertDialogHeader> 42 + <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> 43 + <AlertDialogDescription> 44 + The test ping failed. Are you sure you want to continue to save? 45 + </AlertDialogDescription> 46 + </AlertDialogHeader> 47 + <AlertDialogFooter> 48 + <AlertDialogCancel>Cancel</AlertDialogCancel> 49 + <AlertDialogAction 50 + type="submit" 51 + disabled={isPending} 52 + onClick={(e) => { 53 + e.preventDefault(); 54 + handleSubmit(); 55 + }} 56 + > 57 + {!isPending ? "Confirm" : <LoadingAnimation />} 58 + </AlertDialogAction> 59 + </AlertDialogFooter> 60 + </AlertDialogContent> 61 + </AlertDialog> 62 + ); 63 + };