mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
268 lines
8.7 KiB
TypeScript
268 lines
8.7 KiB
TypeScript
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import { Bell, Plus, Trash2 } from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
import { toast } from "sonner";
|
|
import { Button } from "~/client/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
|
import { Switch } from "~/client/components/ui/switch";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
|
import { Badge } from "~/client/components/ui/badge";
|
|
import {
|
|
getScheduleNotificationsOptions,
|
|
updateScheduleNotificationsMutation,
|
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
|
import { parseError } from "~/client/lib/errors";
|
|
import type { NotificationDestination } from "~/client/lib/types";
|
|
|
|
type Props = {
|
|
scheduleId: number;
|
|
destinations: NotificationDestination[];
|
|
};
|
|
|
|
type NotificationAssignment = {
|
|
destinationId: number;
|
|
notifyOnStart: boolean;
|
|
notifyOnSuccess: boolean;
|
|
notifyOnFailure: boolean;
|
|
};
|
|
|
|
export const ScheduleNotificationsConfig = ({ scheduleId, destinations }: Props) => {
|
|
const [assignments, setAssignments] = useState<Map<number, NotificationAssignment>>(new Map());
|
|
const [hasChanges, setHasChanges] = useState(false);
|
|
const [isAddingNew, setIsAddingNew] = useState(false);
|
|
|
|
const { data: currentAssignments } = useQuery({
|
|
...getScheduleNotificationsOptions({ path: { scheduleId: scheduleId.toString() } }),
|
|
});
|
|
|
|
const updateNotifications = useMutation({
|
|
...updateScheduleNotificationsMutation(),
|
|
onSuccess: () => {
|
|
toast.success("Notification settings saved successfully");
|
|
setHasChanges(false);
|
|
},
|
|
onError: (error) => {
|
|
toast.error("Failed to save notification settings", {
|
|
description: parseError(error)?.message,
|
|
});
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (currentAssignments) {
|
|
const map = new Map<number, NotificationAssignment>();
|
|
for (const assignment of currentAssignments) {
|
|
map.set(assignment.destinationId, {
|
|
destinationId: assignment.destinationId,
|
|
notifyOnStart: assignment.notifyOnStart,
|
|
notifyOnSuccess: assignment.notifyOnSuccess,
|
|
notifyOnFailure: assignment.notifyOnFailure,
|
|
});
|
|
}
|
|
|
|
setAssignments(map);
|
|
}
|
|
}, [currentAssignments]);
|
|
|
|
const addDestination = (destinationId: string) => {
|
|
const id = Number.parseInt(destinationId, 10);
|
|
const newAssignments = new Map(assignments);
|
|
newAssignments.set(id, {
|
|
destinationId: id,
|
|
notifyOnStart: false,
|
|
notifyOnSuccess: false,
|
|
notifyOnFailure: true,
|
|
});
|
|
|
|
setAssignments(newAssignments);
|
|
setHasChanges(true);
|
|
setIsAddingNew(false);
|
|
};
|
|
|
|
const removeDestination = (destinationId: number) => {
|
|
const newAssignments = new Map(assignments);
|
|
newAssignments.delete(destinationId);
|
|
setAssignments(newAssignments);
|
|
setHasChanges(true);
|
|
};
|
|
|
|
const toggleEvent = (destinationId: number, event: "notifyOnStart" | "notifyOnSuccess" | "notifyOnFailure") => {
|
|
const assignment = assignments.get(destinationId);
|
|
if (!assignment) return;
|
|
|
|
const newAssignments = new Map(assignments);
|
|
newAssignments.set(destinationId, {
|
|
...assignment,
|
|
[event]: !assignment[event],
|
|
});
|
|
|
|
setAssignments(newAssignments);
|
|
setHasChanges(true);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
const assignmentsList = Array.from(assignments.values());
|
|
updateNotifications.mutate({
|
|
path: { scheduleId: scheduleId.toString() },
|
|
body: {
|
|
assignments: assignmentsList,
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleReset = () => {
|
|
if (currentAssignments) {
|
|
const map = new Map<number, NotificationAssignment>();
|
|
for (const assignment of currentAssignments) {
|
|
map.set(assignment.destinationId, {
|
|
destinationId: assignment.destinationId,
|
|
notifyOnStart: assignment.notifyOnStart,
|
|
notifyOnSuccess: assignment.notifyOnSuccess,
|
|
notifyOnFailure: assignment.notifyOnFailure,
|
|
});
|
|
}
|
|
setAssignments(map);
|
|
setHasChanges(false);
|
|
}
|
|
};
|
|
|
|
const getDestinationById = (id: number) => {
|
|
return destinations?.find((d) => d.id === id);
|
|
};
|
|
|
|
const availableDestinations = destinations?.filter((d) => !assignments.has(d.id)) || [];
|
|
const assignedDestinations = Array.from(assignments.keys())
|
|
.map((id) => getDestinationById(id))
|
|
.filter((d) => d !== undefined);
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Bell className="h-5 w-5" />
|
|
Notifications
|
|
</CardTitle>
|
|
<CardDescription>Configure which notifications to send for this backup schedule</CardDescription>
|
|
</div>
|
|
{!isAddingNew && availableDestinations.length > 0 && (
|
|
<Button variant="outline" size="sm" onClick={() => setIsAddingNew(true)}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Add notification
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isAddingNew && (
|
|
<div className="mb-6 flex items-center gap-2 max-w-md">
|
|
<Select onValueChange={addDestination}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="Select a notification destination..." />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableDestinations.map((destination) => (
|
|
<SelectItem key={destination.id} value={destination.id.toString()}>
|
|
<div className="flex items-center gap-2">
|
|
<span>{destination.name}</span>
|
|
<span className="text-xs uppercase text-muted-foreground">({destination.type})</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button variant="ghost" size="sm" onClick={() => setIsAddingNew(false)}>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{assignedDestinations.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-8 text-center text-muted-foreground">
|
|
<Bell className="h-8 w-8 mb-2 opacity-20" />
|
|
<p className="text-sm">No notifications configured for this schedule.</p>
|
|
<p className="text-xs mt-1">Click "Add notification" to get started.</p>
|
|
</div>
|
|
) : (
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Destination</TableHead>
|
|
<TableHead className="text-center w-[100px]">Start</TableHead>
|
|
<TableHead className="text-center w-[100px]">Success</TableHead>
|
|
<TableHead className="text-center w-[100px]">Failure</TableHead>
|
|
<TableHead className="w-[50px]"></TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{assignedDestinations.map((destination) => {
|
|
const assignment = assignments.get(destination.id);
|
|
if (!assignment) return null;
|
|
|
|
return (
|
|
<TableRow key={destination.id}>
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium">{destination.name}</span>
|
|
<Badge variant="outline" className="text-[10px] align-middle">
|
|
{destination.type}
|
|
</Badge>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="text-center">
|
|
<Switch
|
|
className="align-middle"
|
|
checked={assignment.notifyOnStart}
|
|
onCheckedChange={() => toggleEvent(destination.id, "notifyOnStart")}
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="text-center">
|
|
<Switch
|
|
className="align-middle"
|
|
checked={assignment.notifyOnSuccess}
|
|
onCheckedChange={() => toggleEvent(destination.id, "notifyOnSuccess")}
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="text-center">
|
|
<Switch
|
|
className="align-middle"
|
|
checked={assignment.notifyOnFailure}
|
|
onCheckedChange={() => toggleEvent(destination.id, "notifyOnFailure")}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => removeDestination(destination.id)}
|
|
className="h-8 w-8 text-muted-foreground hover:text-destructive align-baseline"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
|
|
{hasChanges && (
|
|
<div className="flex gap-2 justify-end mt-4 pt-4">
|
|
<Button variant="outline" size="sm" onClick={handleReset}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="default" size="sm" onClick={handleSave} loading={updateNotifications.isPending}>
|
|
Save changes
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|