mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
add: new notifiers and ui fixes
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user