Openstatus
www.openstatus.dev
1"use client";
2
3import type { Row } from "@tanstack/react-table";
4
5import { QuickActions } from "@/components/dropdowns/quick-actions";
6import {
7 AlertDialog,
8 AlertDialogAction,
9 AlertDialogCancel,
10 AlertDialogContent,
11 AlertDialogDescription,
12 AlertDialogFooter,
13 AlertDialogHeader,
14 AlertDialogTitle,
15} from "@/components/ui/alert-dialog";
16import { getActions } from "@/data/incidents.client";
17import { useTRPC } from "@/lib/trpc/client";
18import type { RouterOutputs } from "@openstatus/api";
19import { useMutation, useQueryClient } from "@tanstack/react-query";
20import { isTRPCClientError } from "@trpc/client";
21import { useMemo, useState, useTransition } from "react";
22import { toast } from "sonner";
23
24type Incident = RouterOutputs["incident"]["list"][number];
25
26interface DataTableRowActionsProps {
27 row: Row<Incident>;
28}
29
30export function DataTableRowActions({ row }: DataTableRowActionsProps) {
31 const [isPending, startTransition] = useTransition();
32 const trpc = useTRPC();
33 const queryClient = useQueryClient();
34 const acknowledgeIncidentMutation = useMutation(
35 trpc.incident.acknowledge.mutationOptions({
36 onSuccess: () => {
37 queryClient.refetchQueries({
38 queryKey: trpc.incident.list.queryKey({
39 monitorId: row.original.monitorId,
40 }),
41 });
42 },
43 }),
44 );
45 const resolveIncidentMutation = useMutation(
46 trpc.incident.resolve.mutationOptions({
47 onSuccess: () => {
48 queryClient.refetchQueries({
49 queryKey: trpc.incident.list.queryKey({
50 monitorId: row.original.monitorId,
51 }),
52 });
53 },
54 }),
55 );
56 const deleteIncidentMutation = useMutation(
57 trpc.incident.delete.mutationOptions({
58 onSuccess: () => {
59 queryClient.refetchQueries({
60 queryKey: trpc.incident.list.queryKey({
61 monitorId: row.original.monitorId,
62 }),
63 });
64 },
65 }),
66 );
67
68 const [type, setType] = useState<"acknowledge" | "resolve" | null>(null);
69 const open = useMemo(() => type !== null, [type]);
70
71 const actions = getActions({
72 acknowledge: row.original.acknowledgedAt
73 ? undefined
74 : () => setType("acknowledge"),
75 resolve: row.original.resolvedAt ? undefined : () => setType("resolve"),
76 });
77
78 const handleConfirm = async () => {
79 try {
80 startTransition(async () => {
81 const promise =
82 type === "acknowledge"
83 ? acknowledgeIncidentMutation.mutateAsync({
84 id: row.original.id,
85 })
86 : resolveIncidentMutation.mutateAsync({
87 id: row.original.id,
88 });
89 toast.promise(promise, {
90 loading: "Confirming...",
91 success: "Confirmed",
92 error: (error) => {
93 if (isTRPCClientError(error)) {
94 return error.message;
95 }
96 return "Failed to confirm";
97 },
98 });
99 await promise;
100 setType(null);
101 });
102 } catch (error) {
103 console.error("Failed to confirm:", error);
104 }
105 };
106
107 return (
108 <>
109 <QuickActions
110 actions={actions}
111 deleteAction={{
112 confirmationValue: row.original.title || "incident",
113 submitAction: async () => {
114 await deleteIncidentMutation.mutateAsync({
115 id: row.original.id,
116 });
117 },
118 }}
119 />
120 <AlertDialog open={open} onOpenChange={() => setType(null)}>
121 <AlertDialogContent
122 onCloseAutoFocus={(event) => {
123 // NOTE: bug where the body is not clickable after closing the alert dialog
124 event.preventDefault();
125 document.body.style.pointerEvents = "";
126 }}
127 >
128 <AlertDialogHeader>
129 <AlertDialogTitle>Confirm your action</AlertDialogTitle>
130 <AlertDialogDescription>
131 You are about to <span className="font-semibold">{type}</span>{" "}
132 this incident.
133 </AlertDialogDescription>
134 </AlertDialogHeader>
135 <AlertDialogFooter>
136 <AlertDialogCancel>Cancel</AlertDialogCancel>
137 <AlertDialogAction
138 onClick={(e) => {
139 e.preventDefault();
140 handleConfirm();
141 }}
142 disabled={isPending}
143 >
144 {isPending ? "Confirming..." : "Confirm"}
145 </AlertDialogAction>
146 </AlertDialogFooter>
147 </AlertDialogContent>
148 </AlertDialog>
149 </>
150 );
151}