Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 684524d59ffaf5eadc7150d42c19be2d2264edfa 363 lines 12 kB view raw
1"use client"; 2 3import * as Portal from "@radix-ui/react-portal"; 4import type { Table } from "@tanstack/react-table"; 5import { useEffect, useState, useTransition } from "react"; 6 7import { Kbd } from "@/components/kbd"; 8import { LoadingAnimation } from "@/components/loading-animation"; 9import { toast, toastAction } from "@/lib/toast"; 10import { cn } from "@/lib/utils"; 11import { api } from "@/trpc/client"; 12import type { Monitor, MonitorTag } from "@openstatus/db/src/schema"; 13import { 14 AlertDialog, 15 AlertDialogAction, 16 AlertDialogCancel, 17 AlertDialogContent, 18 AlertDialogDescription, 19 AlertDialogFooter, 20 AlertDialogHeader, 21 AlertDialogTitle, 22 AlertDialogTrigger, 23 Button, 24 Command, 25 CommandEmpty, 26 CommandGroup, 27 CommandInput, 28 CommandItem, 29 CommandList, 30 Popover, 31 PopoverContent, 32 PopoverTrigger, 33 Select, 34 SelectContent, 35 SelectGroup, 36 SelectItem, 37 SelectLabel, 38 SelectTrigger, 39 SelectValue, 40 Tooltip, 41 TooltipContent, 42 TooltipProvider, 43 TooltipTrigger, 44} from "@openstatus/ui"; 45import { Check, X } from "lucide-react"; 46import { useRouter } from "next/navigation"; 47 48interface DataTableFloatingActions<TData> { 49 table: Table<TData>; 50 actions?: []; 51 tags?: MonitorTag[]; 52} 53 54export function DataTableFloatingActions<TData>({ 55 table, 56 tags, 57}: DataTableFloatingActions<TData>) { 58 const router = useRouter(); 59 const [alertOpen, setAlertOpen] = useState(false); 60 const [isPending, startTransition] = useTransition(); 61 const [method, setMethod] = useState< 62 "delete" | "active" | "public" | "tag" | null 63 >(null); 64 const rows = table.getFilteredSelectedRowModel().rows; 65 66 // clear selection on escape key 67 useEffect(() => { 68 function handleKeyDown(event: KeyboardEvent) { 69 if (event.key === "Escape") { 70 table.toggleAllRowsSelected(false); 71 } 72 } 73 74 window.addEventListener("keydown", handleKeyDown); 75 return () => window.removeEventListener("keydown", handleKeyDown); 76 }, [table]); 77 78 if (table.getFilteredSelectedRowModel().rows.length === 0) { 79 return null; 80 } 81 82 function handleUpdates(props: Partial<Pick<Monitor, "active" | "public">>) { 83 startTransition(async () => { 84 toast.promise( 85 async () => { 86 await api.monitor.updateMonitors.mutate({ 87 ids: rows.map((row) => row.getValue("id")), 88 ...props, 89 }); 90 router.refresh(); 91 }, 92 { 93 loading: "Updating monitor(s)...", 94 success: "Monitor(s) updated!", 95 error: "Something went wrong!", 96 finally: () => {}, 97 }, 98 ); 99 }); 100 } 101 102 function handleTagUpdates(props: { 103 tagId: number; 104 action: "add" | "remove"; 105 }) { 106 startTransition(async () => { 107 toast.promise( 108 async () => { 109 await api.monitor.updateMonitorsTag.mutate({ 110 ids: rows.map((row) => row.getValue("id")), 111 ...props, 112 }); 113 router.refresh(); 114 }, 115 { 116 loading: 117 props.action === "add" 118 ? "Adding tag to monitor(s)..." 119 : "Removing tag from monitor(s)...", 120 success: 121 props.action === "add" 122 ? "Tag added to monitor(s)!" 123 : "Tag removed from monitor(s)", 124 error: "Something went wrong!", 125 finally: () => {}, 126 }, 127 ); 128 }); 129 } 130 131 function handleDeletes() { 132 startTransition(async () => { 133 try { 134 await api.monitor.deleteMonitors.mutate({ 135 ids: rows.map((row) => row.getValue("id")), 136 }); 137 setAlertOpen(false); 138 table.toggleAllRowsSelected(false); 139 router.refresh(); 140 } catch (error) { 141 console.error(error); 142 toastAction("error"); 143 } 144 }); 145 } 146 147 // TODO: can we make it smarter! Its ugly as hell 148 149 const statusValue = rows.every((row) => row.getValue("active") === true) 150 ? "true" 151 : rows.every((row) => row.getValue("active") === false) 152 ? "false" 153 : undefined; 154 155 const visibilityValue = rows.every((row) => row.getValue("public") === true) 156 ? "true" 157 : rows.every((row) => row.getValue("public") === false) 158 ? "false" 159 : undefined; 160 161 const everyTagValue = 162 tags?.filter((tag) => { 163 return rows.every((row) => { 164 const _tags = row.getValue("tags"); 165 if (Array.isArray(_tags)) { 166 return _tags.map(({ id }) => id)?.includes(tag.id); 167 } 168 return false; 169 }); 170 }) || []; 171 172 const someTagValue = 173 tags?.filter((tag) => { 174 return rows.some((row) => { 175 const _tags = row.getValue("tags"); 176 if (Array.isArray(_tags)) { 177 return _tags.map(({ id }) => id)?.includes(tag.id); 178 } 179 return false; 180 }); 181 }) || []; 182 183 return ( 184 <Portal.Root> 185 <div className="fixed inset-x-0 bottom-4 z-50 mx-auto w-fit px-4"> 186 <div className="flex flex-wrap items-center gap-2 rounded-md border bg-background px-4 py-2 shadow-sm"> 187 <TooltipProvider> 188 <Tooltip> 189 <TooltipTrigger asChild> 190 <Button 191 variant="ghost" 192 onClick={() => table.toggleAllRowsSelected(false)} 193 className="border border-dashed" 194 > 195 <span className="whitespace-nowrap"> 196 {rows.length} selected 197 </span> 198 <X className="ml-1.5 size-4 shrink-0" /> 199 </Button> 200 </TooltipTrigger> 201 <TooltipContent className="flex items-center"> 202 <p className="mr-2">Clear selection</p> 203 <Kbd abbrTitle="Escape" variant="outline"> 204 Esc 205 </Kbd> 206 </TooltipContent> 207 </Tooltip> 208 </TooltipProvider> 209 <AlertDialog 210 open={alertOpen} 211 onOpenChange={(value) => setAlertOpen(value)} 212 > 213 <AlertDialogTrigger asChild> 214 <Button 215 variant="outline" 216 className="border-destructive text-destructive hover:bg-destructive hover:text-background" 217 > 218 Delete 219 </Button> 220 </AlertDialogTrigger> 221 <AlertDialogContent> 222 <AlertDialogHeader> 223 <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> 224 <AlertDialogDescription> 225 This action cannot be undone. This will permanently delete the 226 selected monitor(s). 227 </AlertDialogDescription> 228 </AlertDialogHeader> 229 <AlertDialogFooter> 230 <AlertDialogCancel>Cancel</AlertDialogCancel> 231 <AlertDialogAction 232 onClick={() => { 233 setMethod("delete"); 234 handleDeletes(); 235 }} 236 className="bg-destructive text-destructive-foreground hover:bg-destructive/90" 237 > 238 {isPending && method === "delete" ? ( 239 <LoadingAnimation /> 240 ) : ( 241 "Delete" 242 )} 243 </AlertDialogAction> 244 </AlertDialogFooter> 245 </AlertDialogContent> 246 </AlertDialog> 247 <Select 248 disabled={isPending && method === "active"} 249 value={statusValue} 250 onValueChange={(value) => { 251 setMethod("active"); 252 handleUpdates({ active: value === "true" }); 253 }} 254 > 255 <SelectTrigger className="h-9 max-w-fit"> 256 <SelectValue placeholder="Status" /> 257 </SelectTrigger> 258 <SelectContent> 259 <SelectGroup> 260 <SelectLabel>Status</SelectLabel> 261 <SelectItem value="true">Active</SelectItem> 262 <SelectItem value="false">Inactive</SelectItem> 263 </SelectGroup> 264 </SelectContent> 265 </Select> 266 <Popover> 267 <PopoverTrigger disabled={isPending && method === "tag"} asChild> 268 <Button variant="outline" className="flex items-center gap-2"> 269 <span>Tags</span> 270 {everyTagValue.length ? ( 271 <div className="relative flex overflow-hidden"> 272 {everyTagValue.map((tag) => ( 273 <div 274 key={tag.id} 275 style={{ backgroundColor: tag.color }} 276 className="h-2.5 w-2.5 rounded-full ring-2 ring-background" 277 /> 278 ))} 279 </div> 280 ) : null} 281 </Button> 282 </PopoverTrigger> 283 <PopoverContent className="w-[200px] p-0" align="start"> 284 <Command> 285 <CommandInput placeholder={"Tags"} /> 286 <CommandList> 287 <CommandEmpty>No results found.</CommandEmpty> 288 <CommandGroup> 289 {tags?.map((tag) => { 290 const isSelected = everyTagValue 291 .map((tag) => tag.id) 292 ?.includes(tag.id); 293 const isIndeterminated = !isSelected 294 ? someTagValue.map((tag) => tag.id)?.includes(tag.id) 295 : false; 296 return ( 297 <CommandItem 298 key={String(tag.name)} 299 onSelect={() => { 300 setMethod("tag"); 301 handleTagUpdates({ 302 tagId: tag.id, 303 action: 304 !isSelected || isIndeterminated 305 ? "add" 306 : "remove", 307 }); 308 }} 309 > 310 <div 311 className={cn( 312 "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary", 313 { 314 "bg-primary text-primary-foreground": 315 isSelected, 316 "text-muted-foreground": isIndeterminated, 317 "opacity-50 [&_svg]:invisible": 318 !isSelected && !isIndeterminated, 319 }, 320 )} 321 > 322 <Check className={cn("h-4 w-4")} /> 323 </div> 324 <div className="flex w-full items-center justify-between"> 325 <span>{tag.name}</span> 326 <div 327 key={tag.id} 328 style={{ backgroundColor: tag.color }} 329 className="h-2.5 w-2.5 rounded-full" 330 /> 331 </div> 332 </CommandItem> 333 ); 334 })} 335 </CommandGroup> 336 </CommandList> 337 </Command> 338 </PopoverContent> 339 </Popover> 340 <Select 341 disabled={isPending && method === "public"} 342 value={visibilityValue} 343 onValueChange={(value) => { 344 setMethod("public"); 345 handleUpdates({ public: value === "true" }); 346 }} 347 > 348 <SelectTrigger className="h-9 max-w-fit"> 349 <SelectValue placeholder="Visibility" /> 350 </SelectTrigger> 351 <SelectContent> 352 <SelectGroup> 353 <SelectLabel>Visibility</SelectLabel> 354 <SelectItem value="true">Public</SelectItem> 355 <SelectItem value="false">Private</SelectItem> 356 </SelectGroup> 357 </SelectContent> 358 </Select> 359 </div> 360 </div> 361 </Portal.Root> 362 ); 363}