add: new notifiers and ui fixes

This commit is contained in:
Théo LAGACHE
2026-01-07 13:33:23 +01:00
parent 2a1acbfde8
commit df9390847f
30 changed files with 7175 additions and 228 deletions
@@ -1,4 +1,4 @@
import {Form, FormControl, FormField, FormItem, FormMessage, useZodForm} from "@/components/ui/form";
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
import {InfoIcon, Plus, Trash2} from "lucide-react";
import {useFieldArray} from "react-hook-form";
import {
@@ -11,7 +11,6 @@ import {NotificationChannel} from "@/db/schema/09_notification-channel";
import {Label} from "@/components/ui/label";
import {Button} from "@/components/ui/button";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {Separator} from "@/components/ui/separator";
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
import {useMutation} from "@tanstack/react-query";
@@ -22,7 +21,10 @@ import {
} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy.action";
import {useRouter} from "next/navigation";
import {Switch} from "@/components/ui/switch";
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
import {Card} from "@/components/ui/card";
import {cn} from "@/lib/utils";
import Link from "next/link";
type AlertPolicyFormProps = {
onSuccess?: () => void;
@@ -36,9 +38,8 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
const organizationNotificationChannels = notificationChannels.map(channel => channel.id) ?? [];
const formattedAlertPoliciesList = (alertPolicies: AlertPolicy[]) => {
return alertPolicies.filter((alertPolicy)=> organizationNotificationChannels.includes(alertPolicy.notificationChannelId)).map((alertPolicy) => ({
return alertPolicies.filter((alertPolicy) => organizationNotificationChannels.includes(alertPolicy.notificationChannelId)).map((alertPolicy) => ({
notificationChannelId: alertPolicy.notificationChannelId,
eventKinds: alertPolicy.eventKinds,
enabled: alertPolicy.enabled
@@ -48,11 +49,7 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
const form = useZodForm({
schema: AlertPoliciesSchema,
defaultValues: {
alertPolicies: database.alertPolicies && database.alertPolicies.length > 0 ? formattedAlertPoliciesList(database.alertPolicies) : [{
notificationChannelId: "",
eventKinds: [],
enabled: true,
}],
alertPolicies: database.alertPolicies && database.alertPolicies.length > 0 ? formattedAlertPoliciesList(database.alertPolicies) : [],
},
});
@@ -63,7 +60,7 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
const addAlertPolicy = () => {
append({id: "", eventKinds: [], enabled: true});
append({notificationChannelId: "", eventKinds: [], enabled: true});
}
const removeAlertPolicy = (index: number) => {
@@ -78,12 +75,6 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
const mutation = useMutation({
mutationFn: async ({alertPolicies}: AlertPoliciesType) => {
console.log(alertPolicies)
const defaultFormatedAlertPolicies = formattedAlertPoliciesList(database?.alertPolicies ?? []);
const alertPoliciesToAdd = alertPolicies?.filter(
@@ -97,15 +88,10 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
const alertPoliciesToUpdate = alertPolicies?.filter((alertPolicy) => {
const existing = defaultFormatedAlertPolicies.find((a) => a.notificationChannelId === alertPolicy.notificationChannelId);
return existing &&
(existing.eventKinds !== alertPolicy.eventKinds);
(existing.eventKinds !== alertPolicy.eventKinds || existing.enabled !== alertPolicy.enabled);
}) ?? [];
console.log("alertPoliciesToAdd", alertPoliciesToAdd)
console.log("alertPoliciesToRemove", alertPoliciesToRemove)
console.log("alertPoliciesToUpdate", alertPoliciesToUpdate)
const results = await Promise.allSettled([
alertPoliciesToAdd.length > 0
? createAlertPoliciesAction({
@@ -129,8 +115,6 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
: Promise.resolve(null),
]);
console.log(results);
const rejected = results.find((r): r is PromiseRejectedResult => r.status === "rejected");
if (rejected) {
throw new Error(rejected.reason?.message || "Network or server error");
@@ -144,7 +128,6 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
);
console.log("failedActions", failedActions);
if (failedActions.length > 0) {
const firstError = failedActions[0].data.actionError;
const message = firstError?.message || "One or more operations failed";
@@ -155,7 +138,7 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
},
onSuccess: () => {
toast.success("Alert policies saved successfully");
// onSuccess?.();
//onSuccess?.();
router.refresh();
},
onError: (error: any) => {
@@ -167,160 +150,203 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
return (
<Form
form={form}
className="flex flex-col gap-4"
className="flex flex-col gap-6"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">Alert Policies</Label>
<div>
<Label className="text-base font-medium">Configure Alerts</Label>
<p className="text-xs text-muted-foreground mt-1">
Choose which channels receive notifications for specific events.
</p>
</div>
<Button
disabled={
fields.length >= notificationChannels.length ||
notificationChannels.length === 0
}
type="button"
variant="outline" size="sm" onClick={addAlertPolicy}>
<Plus className="w-4 h-4 mr-2"/>
Add
size="sm"
className="h-8"
onClick={addAlertPolicy}>
<Plus className="w-4 h-4 mr-1.5"/>
Add Policy
</Button>
</div>
<div className="space-y-3 w-full">
{organizationNotificationChannels.length === 0 ? (
<div className="text-muted-foreground text-sm text-center border border-dashed rounded-lg p-4">
No alerts policy in organization
<div
className="flex flex-col items-center justify-center p-8 border border-dashed rounded-xl bg-muted/20 text-center gap-2">
<InfoIcon className="h-8 w-8 text-muted-foreground/50"/>
<p className="font-medium text-sm text-foreground">No notification channels</p>
<p className="text-xs text-muted-foreground max-w-xs">
Please <Link href={`/dashboard/settings`} className="underline underline-offset-4 hover:text-primary transition-colors">configure notification channels</Link> in your organization settings first.
</p>
</div>
) : fields.length === 0 ? (
<div className="text-muted-foreground text-sm text-center">
No alert policy, create one
<div
className="flex flex-col items-center justify-center p-8 border border-dashed rounded-xl bg-muted/20 text-center gap-2">
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
<Plus className="h-4 w-4 text-primary"/>
</div>
<p className="font-medium text-sm text-foreground">No alert policies</p>
<p className="text-xs text-muted-foreground">
Click "Add Policy" to start receiving notifications.
</p>
</div>
) : (
fields.map((field, index) => (
<div key={field.id}>
<div className="flex w-full gap-3">
<div className="flex-1">
<FormField
control={form.control}
name={`alertPolicies.${index}.notificationChannelId`}
render={({field}) => {
const selectedIds = form
.watch("alertPolicies")
.map((a: AlertPolicyType) => a.notificationChannelId)
.filter(Boolean);
<div className="grid gap-4">
{fields.map((field, index) => (
<Card key={field.id} className="p-4 transition-all hover:border-primary/50 relative group">
<div className="flex flex-col gap-4">
<div className="grid grid-cols-[1fr_auto_auto] gap-3 items-end">
<div className="flex flex-col gap-1.5">
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">
Notification Channel
</Label>
<FormField
control={form.control}
name={`alertPolicies.${index}.notificationChannelId`}
render={({field}) => {
const selectedIds = form
.watch("alertPolicies")
.map((a: AlertPolicyType) => a.notificationChannelId)
.filter(Boolean);
const availableNotificationChannels = notificationChannels.filter(
(channel) =>
channel.id.toString() === field.value?.toString() ||
!selectedIds.includes(channel.id.toString())
);
const availableNotificationChannels = notificationChannels.filter(
(channel) =>
channel.id.toString() === field.value?.toString() ||
!selectedIds.includes(channel.id.toString())
);
return (
<FormItem>
<Select onValueChange={field.onChange}
value={field.value?.toString() || ""}
>
const selectedChannel = notificationChannels.find(c => c.id === field.value);
return (
<FormItem className="space-y-0">
<Select onValueChange={field.onChange}
value={field.value?.toString() || ""}
>
<FormControl>
<SelectTrigger className="h-9 w-full bg-background border-input">
<SelectValue placeholder="Select channel">
{selectedChannel ? (
<div className="flex items-center gap-2">
<div className={cn("flex items-center justify-center h-4 w-4 rounded")}>
{getNotificationChannelIcon(selectedChannel.provider)}
</div>
<span className="truncate font-medium text-sm">{selectedChannel.name}</span>
<span className="text-[9px] bg-secondary px-1.5 py-0.5 rounded text-muted-foreground ml-auto font-mono uppercase tracking-tighter">{selectedChannel.provider}</span>
</div>
) : "Select channel"}
</SelectValue>
</SelectTrigger>
</FormControl>
<SelectContent>
{availableNotificationChannels.map((channel) => (
<SelectItem key={channel.id.toString()}
value={channel.id.toString()}>
<div className="flex items-center gap-2 w-full">
<div className="text-muted-foreground scale-90">
{getNotificationChannelIcon(channel.provider)}
</div>
<span className="font-medium">{channel.name}</span>
<span
className="text-xs text-muted-foreground ml-2 capitalize">({channel.provider})</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage className="mt-1"/>
</FormItem>
);
}}
/>
</div>
<div className="flex flex-col gap-1.5">
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">
Status
</Label>
<FormField
control={form.control}
name={`alertPolicies.${index}.enabled`}
render={({field}) => (
<FormItem className="space-y-0">
<FormControl>
<SelectTrigger className="h-10 w-full ">
<SelectValue
placeholder="Select notification channel"/>
</SelectTrigger>
<div className="flex items-center h-9 px-3 rounded-md border border-input bg-background min-w-[90px] justify-between">
<Label htmlFor={`switch-${index}`} className="text-xs cursor-pointer font-medium text-foreground mr-2">
{field.value ? "Active" : "Off"}
</Label>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
id={`switch-${index}`}
className="scale-75 origin-right"
/>
</div>
</FormControl>
<SelectContent>
{availableNotificationChannels.map((channel) => (
<SelectItem key={channel.id.toString()}
value={channel.id.toString()}>
{channel.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage className="mb-2"/>
</FormItem>
);
}}
/>
</FormItem>
)}
/>
</div>
<div className="flex flex-col gap-1.5">
<Label className="text-[10px] font-bold text-transparent uppercase tracking-widest select-none">
Actions
</Label>
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9 text-muted-foreground hover:text-destructive hover:border-destructive/50 hover:bg-destructive/10 transition-colors border-input bg-background"
onClick={() => removeAlertPolicy(index)}>
<Trash2 className="w-4 h-4"/>
</Button>
</div>
</div>
<FormField
control={form.control}
name={`alertPolicies.${index}.eventKinds`}
render={({field}) => (
<FormItem>
<FormItem className="space-y-1.5">
<FormLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Trigger Events</FormLabel>
<FormControl>
<MultiSelect
options={EVENT_KIND_OPTIONS}
onValueChange={field.onChange}
defaultValue={field.value ?? []}
placeholder="Select event kinds"
placeholder="Select events to trigger notifications..."
variant="inverted"
animation={0}
className="bg-background/50"
/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
</div>
<div className="flex flex-col gap-3 justify-between items-center ">
<Button type="button" variant="outline"
onClick={() => removeAlertPolicy(index)}>
<Trash2 className="w-4 h-4"/>
</Button>
<div className="flex h-full justify-center items-center">
<Tooltip>
<TooltipTrigger asChild>
<div tabIndex={0} className="inline-flex rounded-md">
<FormField
control={form.control}
name={`alertPolicies.${index}.enabled`}
render={({field}) => (
<FormItem>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</TooltipTrigger>
<TooltipContent className="max-w-64 text-pretty">
<div className="flex items-center gap-1.5">
<InfoIcon className="size-4"/>
<p>This is for activating the alert policy</p>
</div>
</TooltipContent>
</Tooltip>
</div>
</div>
</div>
{index + 1 < fields.length && <Separator className="mt-3"/>}
</div>
))
</Card>
))}
</div>
)}
</div>
</div>
<div className="flex gap-4 justify-end">
<div className="flex gap-3 justify-end pt-2 border-t mt-2">
<ButtonWithLoading variant="outline" type="button" onClick={onCancel}>
Cancel
</ButtonWithLoading>
<ButtonWithLoading isPending={false}>
Save
<ButtonWithLoading isPending={mutation.isPending}>
Save Changes
</ButtonWithLoading>
</div>
</Form>
);
};
};