mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on notification system.
This commit is contained in:
@@ -44,7 +44,7 @@ export function BreadCrumbsWrapper() {
|
||||
)
|
||||
}
|
||||
|
||||
const FORBIDDEN_LINKS = ["organization", "dashboard", "database"];
|
||||
const FORBIDDEN_LINKS = ["organization", "dashboard", "database", "admin", "settings", "notifications"];
|
||||
|
||||
|
||||
export function BreadCrumbs({}: BreadCrumbsProps) {
|
||||
|
||||
@@ -13,7 +13,6 @@ interface CardsWithPaginationProps<T> {
|
||||
cardsPerPage?: number;
|
||||
numberOfColumns?: number;
|
||||
maxVisiblePages?: number;
|
||||
// extendedProps?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/settings-email-tab";
|
||||
import {SettingsStorageTab} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/settings-storage-tab";
|
||||
import {User, UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useRouter, useSearchParams} from "next/navigation";
|
||||
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/admin-user-table";
|
||||
import {
|
||||
AdminOrganizationsTable
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-organizations-tab/admin-organizations-table";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
export type AdminTabsProps = {
|
||||
users: UserWithAccounts[];
|
||||
settings: Setting;
|
||||
organizations: OrganizationWithMembers[];
|
||||
};
|
||||
|
||||
export const AdminTabs = ({users, settings, organizations}: AdminTabsProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "users");
|
||||
|
||||
useEffect(() => {
|
||||
const newTab = searchParams.get("tab") ?? "users";
|
||||
setTab(newTab);
|
||||
}, [searchParams]);
|
||||
|
||||
const handleChangeTab = (value: string) => {
|
||||
router.push(`?tab=${value}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs className="h-full" value={tab} onValueChange={handleChangeTab}>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger className="w-full" value="users">
|
||||
Users
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full" value="organizations">
|
||||
Organizations
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full" value="email">
|
||||
Email
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full" value="storage">
|
||||
Storage
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-full" value="users">
|
||||
<AdminUsersTable users={users}/>
|
||||
</TabsContent>
|
||||
<TabsContent className="h-full" value="organizations">
|
||||
<AdminOrganizationsTable organizations={organizations}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="email">
|
||||
<SettingsEmailTab settings={settings}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="storage">
|
||||
<SettingsStorageTab settings={settings}/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||
import {NotifierCard} from "@/components/wrappers/dashboard/common/notifier/notifier-card/notifier-card";
|
||||
import {useState} from "react";
|
||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
type NotificationChannelsSectionProps = {
|
||||
notificationChannels: NotificationChannelWith[]
|
||||
organizations: OrganizationWithMembers[]
|
||||
}
|
||||
|
||||
export const NotificationChannelsSection = ({
|
||||
organizations,
|
||||
notificationChannels
|
||||
}: NotificationChannelsSectionProps) => {
|
||||
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
|
||||
const hasNotifiers = notificationChannels.length > 0;
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
{hasNotifiers ? (
|
||||
<div className="h-full">
|
||||
<CardsWithPagination
|
||||
data={notificationChannels}
|
||||
cardItem={NotifierCard}
|
||||
cardsPerPage={8}
|
||||
numberOfColumns={2}
|
||||
adminView={true}
|
||||
organizations={organizations}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyStatePlaceholder
|
||||
text="No notification channels configured yet"
|
||||
onClick={() => setIsAddModalOpen(true)}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {Form, FormControl, FormField, FormItem, useZodForm} from "@/components/ui/form";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {
|
||||
NotificationChannelsOrganizationSchema,
|
||||
NotificationChannelsOrganizationType
|
||||
} from "@/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.schema";
|
||||
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
|
||||
import {
|
||||
updateNotificationChannelsOrganizationAction
|
||||
} from "@/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.action";
|
||||
import {toast} from "sonner";
|
||||
|
||||
|
||||
type NotifierChannelOrganisationFormProps = {
|
||||
organizations?: OrganizationWithMembers[];
|
||||
defaultValues?: NotificationChannelWith
|
||||
};
|
||||
|
||||
export const NotifierChannelOrganisationForm = ({
|
||||
organizations,
|
||||
defaultValues,
|
||||
}: NotifierChannelOrganisationFormProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
console.log("defaultValues", defaultValues);
|
||||
console.log("organizations", organizations);
|
||||
|
||||
const defaultOrganizationIds = defaultValues?.organizations?.map(organization => organization.organizationId) ?? []
|
||||
|
||||
|
||||
const form = useZodForm({
|
||||
schema: NotificationChannelsOrganizationSchema,
|
||||
// @ts-ignore
|
||||
defaultValues: {
|
||||
organizations: defaultOrganizationIds
|
||||
},
|
||||
});
|
||||
|
||||
const formatOrganizationsList = (organizations: OrganizationWithMembers[]) => {
|
||||
return organizations
|
||||
.map((organization) => ({
|
||||
value: organization.id,
|
||||
label: `${organization.name}`,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
const mutationUpdateNotificationChannelOrganizations = useMutation({
|
||||
mutationFn: async (values: NotificationChannelsOrganizationType) => {
|
||||
|
||||
const payload = {
|
||||
data: values.organizations,
|
||||
notificationChannelId: defaultValues?.id ?? ""
|
||||
};
|
||||
|
||||
const result = await updateNotificationChannelsOrganizationAction(payload)
|
||||
const inner = result?.data;
|
||||
|
||||
if (inner?.success) {
|
||||
toast.success(inner.actionSuccess?.message);
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error(inner?.actionError?.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutationUpdateNotificationChannelOrganizations.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`organizations`}
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<MultiSelect
|
||||
options={formatOrganizationsList(organizations ?? [])}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ?? []}
|
||||
placeholder="Select organization(s)"
|
||||
variant="inverted"
|
||||
animation={0}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<div className="flex gap-2 justify-end">
|
||||
<ButtonWithLoading isPending={mutationUpdateNotificationChannelOrganizations.isPending}>
|
||||
Save
|
||||
</ButtonWithLoading>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
"use server"
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {db} from "@/db";
|
||||
import {and, eq, inArray} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
|
||||
|
||||
export const updateNotificationChannelsOrganizationAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
data: z.array(z.string()),
|
||||
notificationChannelId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<null>> => {
|
||||
try {
|
||||
const organizationsIds = parsedInput.data;
|
||||
|
||||
const notificationChannel = await db.query.notificationChannel.findFirst({
|
||||
where: eq(drizzleDb.schemas.notificationChannel.id, parsedInput.notificationChannelId),
|
||||
with: {
|
||||
organizations: true,
|
||||
}
|
||||
}) as NotificationChannelWith;
|
||||
|
||||
|
||||
if (!notificationChannel) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Notification channel not found.",
|
||||
status: 404,
|
||||
cause: "not_found",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const existingItemIds = notificationChannel.organizations.map((organization) => organization.organizationId);
|
||||
|
||||
const organizationsToAdd = organizationsIds.filter((id) => !existingItemIds.includes(id));
|
||||
const organizationsToRemove = existingItemIds.filter((id) => !organizationsIds.includes(id));
|
||||
|
||||
if (organizationsToAdd.length > 0) {
|
||||
for (const organizationToAdd of organizationsToAdd) {
|
||||
await db.insert(drizzleDb.schemas.organizationNotificationChannel).values({
|
||||
organizationId: organizationToAdd,
|
||||
notificationChannelId: parsedInput.notificationChannelId
|
||||
});
|
||||
}
|
||||
}
|
||||
if (organizationsToRemove.length > 0) {
|
||||
await db.delete(drizzleDb.schemas.organizationNotificationChannel).where(and(inArray(drizzleDb.schemas.organizationNotificationChannel.organizationId, organizationsToRemove), eq(drizzleDb.schemas.organizationNotificationChannel.notificationChannelId, parsedInput.notificationChannelId))).execute();
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: null,
|
||||
actionSuccess: {
|
||||
message: "Notification channel organizations has been successfully updated.",
|
||||
messageParams: {notificationChannelId: parsedInput.notificationChannelId},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating notification channel:", error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update notification channel.",
|
||||
status: 500,
|
||||
cause: "server_error",
|
||||
messageParams: {message: "Error updating the notification channel"},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const NotificationChannelsOrganizationSchema = z.object({
|
||||
organizations: z.array(z.string())
|
||||
});
|
||||
|
||||
export type NotificationChannelsOrganizationType = z.infer<typeof NotificationChannelsOrganizationSchema>;
|
||||
@@ -0,0 +1,13 @@
|
||||
import {Mail, MessageSquare, Webhook} from "lucide-react";
|
||||
|
||||
export const getNotificationChannelIcon = (type: string) => {
|
||||
const Icon = notificationTypes.find((t) => t.value === type)?.icon
|
||||
return Icon ? <Icon className="h-4 w-4"/> : null
|
||||
}
|
||||
|
||||
|
||||
const notificationTypes = [
|
||||
{value: "smtp", label: "Email", icon: Mail},
|
||||
{value: "slack", label: "Slack", icon: MessageSquare},
|
||||
{value: "webhook", label: "Webhook", icon: Webhook},
|
||||
]
|
||||
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import {ColumnDef} from "@tanstack/react-table";
|
||||
import {NotificationLogWithRelations} from "@/db/services/notification-log";
|
||||
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
||||
import {humanReadableDate} from "@/utils/date-formatting";
|
||||
import {CheckCircle2, XCircle} from "lucide-react";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {NotificationLogModal} from "@/components/wrappers/dashboard/admin/notifications/logs/notification-log-modal";
|
||||
|
||||
|
||||
export function notificationLogsColumns(): ColumnDef<NotificationLogWithRelations>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: "success",
|
||||
header: "Status",
|
||||
cell: ({row}) => {
|
||||
const status = row.original.success ? "delivered" : "failed";
|
||||
return(
|
||||
<Badge variant="outline" className={`gap-1.5 ${getStatusColor(status)}`}>
|
||||
{getStatusIcon(row.original.success)}
|
||||
<span className="capitalize">{status}</span>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "channel",
|
||||
header: "Channel",
|
||||
cell: ({row}) => {
|
||||
const channel = row.original.channel
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="flex h-10 w-10 items-center justify-center rounded-md bg-secondary border border-border">
|
||||
{getNotificationChannelIcon(channel?.provider ?? "")}
|
||||
</div>
|
||||
{channel?.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "sentAt",
|
||||
header: "Sent At",
|
||||
cell: ({row}) => {
|
||||
const sentAt = humanReadableDate(row.original.sentAt)
|
||||
return (
|
||||
<div>
|
||||
{sentAt}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "policy",
|
||||
header: "Event Kinds",
|
||||
cell: ({row}) => {
|
||||
const eventKinds = row.original.policy?.eventKinds ?? [];
|
||||
return (
|
||||
<div>
|
||||
{eventKinds.map((eventKind, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<Badge>{eventKind}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "details",
|
||||
header: "Details",
|
||||
cell: ({row}) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NotificationLogModal notificationLog={row.original} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
const getStatusIcon = (status: boolean) => {
|
||||
switch (status) {
|
||||
case true:
|
||||
return <CheckCircle2 className="h-4 w-4"/>
|
||||
case false:
|
||||
return <XCircle className="h-4 w-4"/>
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "delivered":
|
||||
return "bg-green-100 dark:bg-green-100/10"
|
||||
case "failed":
|
||||
return "bg-red-100 dark:bg-red-100/10"
|
||||
case "pending":
|
||||
return "bg-orange-100 dark:bg-orange-100/10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client"
|
||||
import {Eye} from "lucide-react";
|
||||
|
||||
import {useState} from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from "@/components/ui/dialog";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {NotificationLogWithRelations} from "@/db/services/notification-log";
|
||||
|
||||
type NotificationLogModalProps = {
|
||||
notificationLog: NotificationLogWithRelations;
|
||||
}
|
||||
|
||||
export const NotificationLogModal = ({notificationLog}: NotificationLogModalProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" onClick={() => setOpen(true)} className="relative">
|
||||
<Eye/>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Notification details</DialogTitle>
|
||||
<DialogDescription>
|
||||
Details of the notification sent
|
||||
</DialogDescription>
|
||||
<Separator className="mt-3 mb-3"/>
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
<div>
|
||||
<span className="font-semibold">Title:</span>{" "}
|
||||
<span>{notificationLog.content.title}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Message:</span>{" "}
|
||||
<span>{notificationLog.content.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{notificationLog.payload && (
|
||||
<>
|
||||
<Separator className="mb-3" />
|
||||
<div className="text-sm font-mono bg-gray-100 dark:bg-gray-800 dark:text-gray-200 p-4 rounded-md overflow-auto">
|
||||
{JSON.stringify(notificationLog.payload, null, 2)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {
|
||||
notificationLogsColumns,
|
||||
} from "@/components/wrappers/dashboard/admin/notifications/logs/columns";
|
||||
import {NotificationLogWithRelations} from "@/db/services/notification-log";
|
||||
|
||||
|
||||
type NotificationsLogsListProps = {
|
||||
notificationLogs: NotificationLogWithRelations[]
|
||||
}
|
||||
|
||||
export const NotificationLogsList = (props: NotificationsLogsListProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
enableSelect={false}
|
||||
columns={notificationLogsColumns()}
|
||||
data={props.notificationLogs}
|
||||
enablePagination
|
||||
/>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organization/admin-orgnization-list";
|
||||
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-orgnization-list";
|
||||
|
||||
export type AdminOrganizationsTableProps = {
|
||||
organizations: OrganizationWithMembers[];
|
||||
-1
@@ -12,7 +12,6 @@ import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with
|
||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/button-delete-use";
|
||||
import {Organization} from "@/db/schema/03_organization";
|
||||
|
||||
export const organizationsColumnsAdmin: ColumnDef<Organization>[] = [
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { OrganizationSchema } from "@/components/wrappers/dashboard/admin/organization/organization.schema";
|
||||
import { OrganizationSchema } from "@/components/wrappers/dashboard/admin/organizations/organization/organization.schema";
|
||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth/auth-client";
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AdminOrganizationList } from "@/components/wrappers/dashboard/admin/organization/admin-orgnization-list";
|
||||
import { AdminOrganizationAddModal } from "@/components/wrappers/dashboard/admin/organization/admin-organization-add-modal";
|
||||
import { AdminOrganizationList } from "@/components/wrappers/dashboard/admin/organizations/organization/admin-orgnization-list";
|
||||
import { AdminOrganizationAddModal } from "@/components/wrappers/dashboard/admin/organizations/organization/admin-organization-add-modal";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
type AdminOrganizationSectionProps = {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
import { DataTable } from "@/components/wrappers/common/table/data-table";
|
||||
import { organizationsListColumns } from "@/components/wrappers/dashboard/admin/organization/table-colums";
|
||||
import { organizationsListColumns } from "@/components/wrappers/dashboard/admin/organizations/organization/table-colums";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
type AdminOrganizationListProps = {
|
||||
+2
-2
@@ -7,11 +7,11 @@ import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodFo
|
||||
import {
|
||||
AddMemberSchema,
|
||||
AddMemberSchemaType
|
||||
} from "@/components/wrappers/dashboard/admin/organization/organization.schema";
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/organization.schema";
|
||||
import {SearchInput} from "@/components/ui/search-input";
|
||||
import {
|
||||
addMemberOrganizationAction
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/add-member.action";
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/add-member.action";
|
||||
import {toast} from "sonner";
|
||||
import {OrganizationWithMembers, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
+19
-9
@@ -1,11 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { OrganizationAddMemberForm } from "@/components/wrappers/dashboard/admin/organization/details/organization-add-member-form";
|
||||
import { useState } from "react";
|
||||
import { UserPlus } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {OrganizationWithMembers, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
OrganizationAddMemberForm
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/organization-add-member-form";
|
||||
import {useState} from "react";
|
||||
import {UserPlus} from "lucide-react";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import { OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
|
||||
type OrganizationAddMemberModalProps = {
|
||||
@@ -13,13 +22,13 @@ type OrganizationAddMemberModalProps = {
|
||||
organization: OrganizationWithMembersAndUsers;
|
||||
};
|
||||
|
||||
export const OrganizationAddMemberModal = ({ users, organization }: OrganizationAddMemberModalProps) => {
|
||||
export const OrganizationAddMemberModal = ({users, organization}: OrganizationAddMemberModalProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<UserPlus className="w-4 h-4 mr-2" />
|
||||
<UserPlus className="w-4 h-4 mr-2"/>
|
||||
Add member
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -28,7 +37,8 @@ export const OrganizationAddMemberModal = ({ users, organization }: Organization
|
||||
<DialogTitle>Add member to your organization</DialogTitle>
|
||||
<DialogDescription>Select a user to add to your organization</DialogDescription>
|
||||
</DialogHeader>
|
||||
<OrganizationAddMemberForm users={users} organization={organization} onSuccessAction={() => setOpen(!open)} />
|
||||
<OrganizationAddMemberForm users={users} organization={organization}
|
||||
onSuccessAction={() => setOpen(!open)}/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
+2
-2
@@ -12,12 +12,12 @@ import {Button} from "@/components/ui/button";
|
||||
import {MoreHorizontal, Settings, Trash2} from "lucide-react";
|
||||
import {
|
||||
OrganizationDeleteMemberModal
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-delete-member-modal";
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/organization-delete-member-modal";
|
||||
import {useState} from "react";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {
|
||||
OrganizationMemberChangeRoleModal
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-member-change-role";
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/organization-member-change-role";
|
||||
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
|
||||
type OrganizationMemberCardProps = {
|
||||
+1
-1
@@ -22,7 +22,7 @@ import {updateMemberRoleAction} from "@/components/wrappers/dashboard/settings/u
|
||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
||||
import {
|
||||
updateMemberRoleAdminAction
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/role-member.action";
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/role-member.action";
|
||||
|
||||
type OrganizationMemberChangeRoleModalProps = {
|
||||
open: boolean;
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodFo
|
||||
import {
|
||||
UpdateOrganizationSchema,
|
||||
UpdateOrganizationSchemaType
|
||||
} from "@/components/wrappers/dashboard/admin/organization/organization.schema";
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/organization.schema";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
+3
-3
@@ -6,17 +6,17 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {
|
||||
UpdateOrganizationForm
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/update-organization-form";
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/update-organization-form";
|
||||
import {
|
||||
OrganizationMemberCard
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-member-card";
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/organization-member-card";
|
||||
import {useRouter, useSearchParams} from "next/navigation";
|
||||
import {useEffect, useState} from "react";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
import {
|
||||
OrganizationAddMemberModal
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-add-member-modal";
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/organization-add-member-modal";
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
type OrganizationManagementProps = {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import {ColumnDef} from "@tanstack/react-table";
|
||||
import {ButtonDeleteOrganization} from "@/components/wrappers/dashboard/admin/organization/button-delete-organization";
|
||||
import {ButtonDeleteOrganization} from "@/components/wrappers/dashboard/admin/organizations/organization/button-delete-organization";
|
||||
import Link from "next/link";
|
||||
import {Settings} from "lucide-react";
|
||||
import {buttonVariants} from "@/components/ui/button";
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
import { z } from "zod";
|
||||
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form.schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
@@ -19,11 +19,11 @@ import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {
|
||||
EmailFormSchema,
|
||||
EmailFormType
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
} from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form.schema";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import {
|
||||
updateEmailSettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.action";
|
||||
} from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
import {EmailForm} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form";
|
||||
import {EmailForm} from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {EmailFormType} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
import {EmailFormType} from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form.schema";
|
||||
|
||||
|
||||
export type SettingsEmailSectionProps = {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {toast} from "sonner";
|
||||
|
||||
export type AdminSettingsTabProps = {};
|
||||
|
||||
export const AdminSettingsTab = (props: AdminSettingsTabProps) => {
|
||||
export const AdminSettingsSection = (props: AdminSettingsTabProps) => {
|
||||
|
||||
const handleDownloadKey = async () => {
|
||||
|
||||
+7
-7
@@ -1,8 +1,9 @@
|
||||
"use client"
|
||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
||||
import {Info, ShieldCheck} from "lucide-react";
|
||||
import {Switch} from "@/components/ui/switch";
|
||||
import {Label} from "@/components/ui/label";
|
||||
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/storage-s3-form";
|
||||
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/storage-s3-form";
|
||||
import {useState} from "react";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
@@ -11,15 +12,15 @@ import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {
|
||||
updateStorageSettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.action";
|
||||
} from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.action";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {S3FormType} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import {S3FormType} from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.schema";
|
||||
|
||||
export type SettingsStorageTabProps = {
|
||||
export type SettingsStorageSectionProps = {
|
||||
settings: Setting;
|
||||
};
|
||||
|
||||
export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
||||
export const SettingsStorageSection = (props: SettingsStorageSectionProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
@@ -62,8 +63,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full py-4">
|
||||
<h1>Settings for Portabase storage</h1>
|
||||
<div className="flex flex-col h-full">
|
||||
<Alert className="mt-3">
|
||||
<Info className="h-4 w-4"/>
|
||||
<AlertTitle>Informations</AlertTitle>
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { db } from "@/db";
|
||||
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
+2
-2
@@ -8,10 +8,10 @@ import { Button } from "@/components/ui/button";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.schema";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.action";
|
||||
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.action";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
|
||||
export type S3FormProps = {
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
"use server";
|
||||
import { z } from "zod";
|
||||
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
|
||||
export const updateEmailSettingsAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
data: EmailFormSchema,
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const { name, data } = parsedInput;
|
||||
|
||||
const [updatedSettings] = await db
|
||||
.update(drizzleDb.schemas.setting)
|
||||
.set({
|
||||
...data,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.setting.name, name))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
data: updatedSettings,
|
||||
};
|
||||
});
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const EmailFormSchema = z.object({
|
||||
smtpPassword: z.string(),
|
||||
smtpFrom: z.string(),
|
||||
smtpHost: z.string(),
|
||||
smtpPort: z.string(),
|
||||
smtpUser: z.string(),
|
||||
});
|
||||
|
||||
export type EmailFormType = z.infer<typeof EmailFormSchema>;
|
||||
-208
@@ -1,208 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useZodForm
|
||||
} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Form} from "@/components/ui/form";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
|
||||
import {
|
||||
EmailFormSchema,
|
||||
EmailFormType
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import {
|
||||
updateEmailSettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {Send} from "lucide-react";
|
||||
import {sendEmail} from "@/lib/email/email-helper";
|
||||
import {render} from "@react-email/render";
|
||||
import TestEmailSettings from "@/components/emails/email-settings-test";
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
export type EmailFormProps = {
|
||||
defaultValues?: EmailFormType;
|
||||
};
|
||||
|
||||
export const EmailForm = (props: EmailFormProps) => {
|
||||
const router = useRouter();
|
||||
const form = useZodForm({
|
||||
schema: EmailFormSchema,
|
||||
defaultValues: props.defaultValues,
|
||||
});
|
||||
const isDirty = form.formState.isDirty;
|
||||
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: EmailFormType) => {
|
||||
const updateEmailSettings = await updateEmailSettingsAction({name: "system", data: values});
|
||||
const data = updateEmailSettings?.data?.data;
|
||||
if (updateEmailSettings?.serverError || !data) {
|
||||
toast.error(updateEmailSettings?.serverError);
|
||||
return;
|
||||
}
|
||||
toast.success(`Success updating email informations`);
|
||||
router.refresh();
|
||||
form.reset(data)
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const mutationSendEmailTest = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!props.defaultValues?.smtpUser || !props.defaultValues?.smtpFrom) {
|
||||
toast.error("SMTP is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
const email = await sendEmail({
|
||||
to: props.defaultValues.smtpUser,
|
||||
|
||||
subject: "Portabase",
|
||||
html: await render(TestEmailSettings(), {}),
|
||||
from: props.defaultValues.smtpFrom,
|
||||
});
|
||||
if (email.response) {
|
||||
toast.success("Test Email Successfully sent !");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpFrom"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>From Email *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"exemple@portabase.com"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"The email from where the email will be send"}</FormDescription>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpHost"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server Host *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"ssl0.ovh.net"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"Your email server host"}</FormDescription>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpPort"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server Port *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"465"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"Your email server port (send)"}</FormDescription>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpPassword"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput placeholder="Password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"Your email server password"}</FormDescription>
|
||||
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpUser"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>User Email *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"exemple@portabase.com"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"The email server user"}</FormDescription>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-between gap-4 mt-5">
|
||||
{props.defaultValues?.smtpFrom && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<ButtonWithLoading
|
||||
type="button"
|
||||
disabled={isDirty || mutationSendEmailTest.isPending}
|
||||
isPending={mutationSendEmailTest.isPending}
|
||||
onClick={async () => {
|
||||
await mutationSendEmailTest.mutateAsync();
|
||||
}}
|
||||
icon={<Send/>}
|
||||
size="default"
|
||||
>
|
||||
Send email test
|
||||
</ButtonWithLoading>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
{isDirty && (
|
||||
<TooltipContent className={cn(!isDirty && "hidden")}>
|
||||
You must save changes before testing the email settings.
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
import {EmailForm} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form";
|
||||
import {Send} from "lucide-react";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {sendEmail} from "@/lib/email/email-helper";
|
||||
import {render} from "@react-email/render";
|
||||
import {toast} from "sonner";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {EmailFormType} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
import TestEmailSettings from "@/components/emails/email-settings-test"
|
||||
|
||||
|
||||
export type SettingsEmailTabProps = {
|
||||
settings: Setting;
|
||||
};
|
||||
|
||||
export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!props.settings.smtpUser || !props.settings.smtpFrom) {
|
||||
toast.error("SMTP is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
const email = await sendEmail({
|
||||
to: props.settings.smtpUser,
|
||||
subject: "Portabase",
|
||||
html: await render(TestEmailSettings(), {}),
|
||||
from: props.settings.smtpFrom,
|
||||
});
|
||||
if (email.response) {
|
||||
toast.success("Test Email Successfully sent !");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleSendMailTest = async () => {
|
||||
await mutation.mutateAsync();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full py-4">
|
||||
<div className="flex gap-4 h-fit justify-between">
|
||||
<h1>Settings for Portabase email setup</h1>
|
||||
{props.settings.smtpFrom && (
|
||||
<ButtonWithLoading
|
||||
isPending={mutation.isPending}
|
||||
onClick={async () => {
|
||||
await handleSendMailTest();
|
||||
}}
|
||||
icon={<Send/>}
|
||||
size="default"
|
||||
>Send email test</ButtonWithLoading>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings as EmailFormType : undefined}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/columns-users";
|
||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/users/columns-users";
|
||||
|
||||
export type AdminUsersTableProps = {
|
||||
users: UserWithAccounts[];
|
||||
+1
-1
@@ -9,7 +9,7 @@ import {useState} from "react";
|
||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/button-delete-use";
|
||||
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/users/button-delete-use";
|
||||
import {formatLocalizedDate} from "@/utils/date-formatting";
|
||||
|
||||
export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||
@@ -8,33 +8,47 @@ import {
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {NotifierForm} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
import {useState} from "react";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {
|
||||
NotifierChannelOrganisationForm
|
||||
} from "@/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization-form";
|
||||
|
||||
type OrganizationNotifierAddModalProps = {
|
||||
notificationChannel?: NotificationChannel
|
||||
notificationChannel?: NotificationChannelWith
|
||||
organization?: OrganizationWithMembers;
|
||||
open: boolean;
|
||||
onOpenChangeAction: (open: boolean) => void;
|
||||
open?: boolean;
|
||||
onOpenChangeAction?: (open: boolean) => void;
|
||||
adminView?: boolean;
|
||||
organizations?: OrganizationWithMembers[]
|
||||
}
|
||||
|
||||
|
||||
export const NotifierAddEditModal = ({
|
||||
organization,
|
||||
notificationChannel,
|
||||
open,
|
||||
onOpenChangeAction
|
||||
open = false,
|
||||
onOpenChangeAction,
|
||||
adminView,
|
||||
organizations
|
||||
}: OrganizationNotifierAddModalProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const [openInternal, setOpen] = useState(open);
|
||||
|
||||
const isCreate = !Boolean(notificationChannel);
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChangeAction}>
|
||||
<Dialog open={openInternal} onOpenChange={(state) => {
|
||||
onOpenChangeAction?.(state);
|
||||
setOpen(state);
|
||||
}}>
|
||||
<DialogTrigger asChild>
|
||||
{isCreate ?
|
||||
<Button>
|
||||
<Plus/>{!isMobile && `Add notification channel` }
|
||||
<Plus/>{!isMobile && `Add notification channel`}
|
||||
</Button>
|
||||
:
|
||||
<Button
|
||||
@@ -52,11 +66,52 @@ export const NotifierAddEditModal = ({
|
||||
Configure your notification channel preferences and event triggers.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<NotifierForm
|
||||
defaultValues={notificationChannel}
|
||||
organization={organization}
|
||||
onSuccessAction={() => onOpenChangeAction(false)}
|
||||
/>
|
||||
|
||||
|
||||
<div>
|
||||
{adminView ?
|
||||
|
||||
<Tabs className="flex flex-col flex-1" defaultValue="configuration">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="configuration">Configuration</TabsTrigger>
|
||||
<TabsTrigger value="organizations">Organizations</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-full justify-between" value="configuration">
|
||||
<NotifierForm
|
||||
adminView={adminView}
|
||||
defaultValues={notificationChannel}
|
||||
organization={organization}
|
||||
onSuccessAction={() => {
|
||||
onOpenChangeAction?.(false)
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent className="h-full justify-between" value="organizations">
|
||||
|
||||
<NotifierChannelOrganisationForm
|
||||
defaultValues={notificationChannel}
|
||||
organizations={organizations}
|
||||
/>
|
||||
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
:
|
||||
<NotifierForm
|
||||
adminView={adminView}
|
||||
defaultValues={notificationChannel}
|
||||
organization={organization}
|
||||
onSuccessAction={() => {
|
||||
onOpenChangeAction?.(false)
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
+13
-4
@@ -2,7 +2,7 @@
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal";
|
||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {Switch} from "@/components/ui/switch";
|
||||
import {
|
||||
updateNotificationChannelAction
|
||||
@@ -12,11 +12,18 @@ import {useState} from "react";
|
||||
import {Organization, OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
export type EditNotifierButtonProps = {
|
||||
notificationChannel: NotificationChannel;
|
||||
organization? : OrganizationWithMembers;
|
||||
notificationChannel: NotificationChannelWith;
|
||||
organization?: OrganizationWithMembers;
|
||||
organizations?: OrganizationWithMembers[];
|
||||
adminView?: boolean;
|
||||
};
|
||||
|
||||
export const EditNotifierButton = ({notificationChannel, organization}: EditNotifierButtonProps) => {
|
||||
export const EditNotifierButton = ({
|
||||
organizations,
|
||||
adminView = false,
|
||||
notificationChannel,
|
||||
organization
|
||||
}: EditNotifierButtonProps) => {
|
||||
const router = useRouter();
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
|
||||
@@ -53,6 +60,8 @@ export const EditNotifierButton = ({notificationChannel, organization}: EditNoti
|
||||
}}
|
||||
/>
|
||||
<NotifierAddEditModal
|
||||
organizations={organizations}
|
||||
adminView={adminView}
|
||||
organization={organization}
|
||||
notificationChannel={notificationChannel}
|
||||
open={isAddModalOpen}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import {Card} from "@/components/ui/card";
|
||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {Mail, MessageSquare, Pencil, Trash2, Webhook} from "lucide-react";
|
||||
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {Switch} from "@/components/ui/switch";
|
||||
import {
|
||||
DeleteNotifierButton
|
||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-card/button-delete-notifier";
|
||||
import {EditNotifierButton} from "@/components/wrappers/dashboard/common/notifier/notifier-card/button-edit-notifier";
|
||||
import {Organization, OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
||||
|
||||
export type NotifierCardProps = {
|
||||
data: NotificationChannel;
|
||||
data: NotificationChannelWith;
|
||||
organization?: OrganizationWithMembers;
|
||||
organizations?: OrganizationWithMembers[];
|
||||
adminView?: boolean;
|
||||
};
|
||||
|
||||
export const NotifierCard = (props: NotifierCardProps) => {
|
||||
@@ -25,7 +26,7 @@ export const NotifierCard = (props: NotifierCardProps) => {
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="flex h-10 w-10 items-center justify-center rounded-md bg-secondary border border-border">
|
||||
{getIcon(data.provider)}
|
||||
{getNotificationChannelIcon(data.provider)}
|
||||
</div>
|
||||
<div className={`h-2 w-2 rounded-full ${data.enabled ? "bg-green-600" : "bg-muted"}`}/>
|
||||
</div>
|
||||
@@ -41,6 +42,8 @@ export const NotifierCard = (props: NotifierCardProps) => {
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<EditNotifierButton
|
||||
organizations={props.organizations}
|
||||
adminView={props.adminView}
|
||||
organization={organization}
|
||||
notificationChannel={data}/>
|
||||
<DeleteNotifierButton
|
||||
@@ -54,14 +57,3 @@ export const NotifierCard = (props: NotifierCardProps) => {
|
||||
};
|
||||
|
||||
|
||||
const getIcon = (type: string) => {
|
||||
const Icon = notificationTypes.find((t) => t.value === type)?.icon
|
||||
return Icon ? <Icon className="h-4 w-4"/> : null
|
||||
}
|
||||
|
||||
|
||||
const notificationTypes = [
|
||||
{value: "smtp", label: "Email", icon: Mail},
|
||||
{value: "slack", label: "Slack", icon: MessageSquare},
|
||||
{value: "webhook", label: "Webhook", icon: Webhook},
|
||||
]
|
||||
+1
-1
@@ -7,7 +7,7 @@ export const NotificationChannelFormSchema = z.object({
|
||||
.min(5, "Name must be at least 5 characters long")
|
||||
.max(40, "Name must be at most 40 characters long"),
|
||||
|
||||
provider: z.enum(["curl", "slack", "smtp", "webhook"], {
|
||||
provider: z.enum(["slack", "smtp"], {
|
||||
required_error: "Provider is required",
|
||||
}),
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
NotifierSlackForm
|
||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-slack.form";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {
|
||||
NotifierTestChannelButton
|
||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button";
|
||||
@@ -30,13 +30,16 @@ import {useEffect} from "react";
|
||||
type NotifierFormProps = {
|
||||
onSuccessAction?: () => void;
|
||||
organization?: OrganizationWithMembers;
|
||||
defaultValues?: NotificationChannel
|
||||
defaultValues?: NotificationChannelWith
|
||||
adminView?: boolean
|
||||
};
|
||||
|
||||
export const NotifierForm = ({onSuccessAction, organization, defaultValues}: NotifierFormProps) => {
|
||||
export const NotifierForm = ({onSuccessAction, organization, defaultValues, adminView}: NotifierFormProps) => {
|
||||
|
||||
const isCreate = !Boolean(defaultValues);
|
||||
const router = useRouter();
|
||||
const isCreate = !Boolean(defaultValues);
|
||||
|
||||
|
||||
const form = useZodForm({
|
||||
schema: NotificationChannelFormSchema,
|
||||
// @ts-ignore
|
||||
@@ -47,7 +50,7 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
||||
form.reset(defaultValues ? {...defaultValues} : {});
|
||||
}, [defaultValues]);
|
||||
|
||||
const mutationCreateOrganisation = useMutation({
|
||||
const mutationAddNotificationChannel = useMutation({
|
||||
mutationFn: async (values: NotificationChannelFormType) => {
|
||||
|
||||
const payload = {
|
||||
@@ -73,12 +76,14 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
||||
|
||||
const provider = form.watch("provider");
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutationCreateOrganisation.mutateAsync(values);
|
||||
await mutationAddNotificationChannel.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
@@ -136,7 +141,8 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
{defaultValues && (
|
||||
<NotifierTestChannelButton organizationId={organization?.id} notificationChannel={defaultValues}/>
|
||||
<NotifierTestChannelButton organizationId={organization?.id}
|
||||
notificationChannel={defaultValues}/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -150,7 +156,7 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<ButtonWithLoading isPending={mutationCreateOrganisation.isPending}>
|
||||
<ButtonWithLoading isPending={mutationAddNotificationChannel.isPending}>
|
||||
{isCreate ? "Add" : "Save"} Channel
|
||||
</ButtonWithLoading>
|
||||
</div>
|
||||
@@ -159,7 +165,3 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues}: Not
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Layers,
|
||||
ChartArea,
|
||||
ShieldHalf,
|
||||
Building, UserRoundCog, Mail, PackageOpen
|
||||
Building, UserRoundCog, Mail, PackageOpen, Logs, Megaphone, Blocks
|
||||
} from "lucide-react";
|
||||
import {SidebarGroupItem, SidebarMenuCustomBase} from "@/components/wrappers/dashboard/common/sidebar/menu-sidebar";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
@@ -69,6 +69,17 @@ export const SidebarMenuCustomMain = () => {
|
||||
details: true,
|
||||
type: "item"
|
||||
},
|
||||
{
|
||||
title: "Notifications",
|
||||
url: "/notifications",
|
||||
icon: Megaphone,
|
||||
details: true,
|
||||
type: "collapse",
|
||||
submenu: [
|
||||
{ title: "Channels", url: "/notifications/channels", icon: Blocks, type: "item" },
|
||||
{ title: "Activity Logs", url: "/notifications/logs", icon: Logs, type: "item" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Access management",
|
||||
url: "/admin",
|
||||
|
||||
@@ -12,8 +12,8 @@ import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile/us
|
||||
import { toast } from "sonner";
|
||||
import { updateUserAction } from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||
import {sessionsColumns} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/sessions/table-columns";
|
||||
import {accountsColumns} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/accounts/table-columns";
|
||||
import {sessionsColumns} from "@/components/wrappers/dashboard/admin/users/sessions/table-columns";
|
||||
import {accountsColumns} from "@/components/wrappers/dashboard/admin/users/accounts/table-columns";
|
||||
import {Account, Session} from "better-auth";
|
||||
|
||||
export type UserFormProps = {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {boolean, jsonb, pgEnum, pgTable, primaryKey, unique, uuid, varchar} from "drizzle-orm/pg-core";
|
||||
import {timestamps} from "@/db/schema/00_common";
|
||||
import {organization} from "@/db/schema/03_organization";
|
||||
import {MemberWithUser, Organization, organization} from "@/db/schema/03_organization";
|
||||
import {relations} from "drizzle-orm";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
import {OrganizationInvitation} from "@/db/schema/05_invitation";
|
||||
|
||||
|
||||
export const providerKindEnum = pgEnum('provider_kind', ['slack', 'smtp']);
|
||||
@@ -47,4 +48,12 @@ export const organizationNotificationChannelRelations = relations(organizationNo
|
||||
}));
|
||||
|
||||
export const notificationChannelSchema = createSelectSchema(notificationChannel);
|
||||
export type NotificationChannel = z.infer<typeof notificationChannelSchema>;
|
||||
export type NotificationChannel = z.infer<typeof notificationChannelSchema>;
|
||||
|
||||
|
||||
export type NotificationChannelWith = NotificationChannel & {
|
||||
organizations: {
|
||||
organizationId: string;
|
||||
notificationChannelId: string;
|
||||
}[];
|
||||
};
|
||||
@@ -1,8 +1,11 @@
|
||||
import {pgTable, uuid, timestamp, jsonb, varchar, boolean, text, pgEnum} from 'drizzle-orm/pg-core';
|
||||
import {notificationChannel, providerKindEnum} from "@/db/schema/09_notification-channel";
|
||||
import {notificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {alertPolicy} from "@/db/schema/10_alert-policy";
|
||||
import {organization} from "@/db/schema/03_organization";
|
||||
import {timestamps} from "@/db/schema/00_common";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
|
||||
|
||||
export const levelEnum = pgEnum('level', ['critical', 'warning', 'info']);
|
||||
|
||||
@@ -12,21 +15,27 @@ export const notificationLog = pgTable('notification_log', {
|
||||
|
||||
channelId: uuid('channel_id')
|
||||
.notNull()
|
||||
.references(() => notificationChannel.id, { onDelete: 'restrict' }),
|
||||
.references(() => notificationChannel.id, {onDelete: 'restrict'}),
|
||||
policyId: uuid('policy_id')
|
||||
.references(() => alertPolicy.id, { onDelete: 'restrict' }),
|
||||
.references(() => alertPolicy.id, {onDelete: 'restrict'}),
|
||||
organizationId: uuid('organization_id')
|
||||
.references(() => organization.id, { onDelete: 'set null' }),
|
||||
.references(() => organization.id, {onDelete: 'set null'}),
|
||||
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
title: varchar('title', {length: 255}).notNull(),
|
||||
message: text('message').notNull(),
|
||||
level: levelEnum('level').notNull(),
|
||||
payload: jsonb('payload'),
|
||||
|
||||
|
||||
success: boolean('success').notNull(),
|
||||
error: text('error'),
|
||||
providerResponse: jsonb('provider_response'),
|
||||
sentAt: timestamp('sent_at').defaultNow().notNull(),
|
||||
|
||||
...timestamps
|
||||
});
|
||||
});
|
||||
|
||||
export const notificationLogSchema = createSelectSchema(notificationLog);
|
||||
export type NotificationLog = z.infer<typeof notificationLogSchema>;
|
||||
|
||||
export type NotificationLevel = (typeof levelEnum.enumValues)[number];
|
||||
|
||||
@@ -1,24 +1,45 @@
|
||||
import {desc, eq, and, gte, lte} from 'drizzle-orm';
|
||||
import {
|
||||
notificationLog
|
||||
} from "@/db/schema/11_notification-log";
|
||||
import {
|
||||
notificationChannel,
|
||||
} from "@/db/schema/09_notification-channel";
|
||||
import {and, desc, eq, gte, lte} from 'drizzle-orm';
|
||||
import {NotificationLevel, notificationLog} from "@/db/schema/11_notification-log";
|
||||
import {notificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {db} from "@/db";
|
||||
import {alertPolicy} from "@/db/schema/10_alert-policy";
|
||||
import {Json} from "drizzle-zod";
|
||||
|
||||
export type NotificationLogWithRelations = {
|
||||
id: string;
|
||||
title: string;
|
||||
level: NotificationLevel;
|
||||
success: boolean;
|
||||
error: string | null;
|
||||
sentAt: Date;
|
||||
payload: Json | null;
|
||||
content: {
|
||||
title: string;
|
||||
message: string;
|
||||
},
|
||||
channel: {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
} | null;
|
||||
policy: {
|
||||
id: string;
|
||||
eventKinds: string[];
|
||||
} | null;
|
||||
};
|
||||
|
||||
export async function getNotificationHistory(filters?: {
|
||||
channelId?: string;
|
||||
policyId?: string;
|
||||
organizationId?: string;
|
||||
level?: string;
|
||||
success?: boolean;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
limit?: number;
|
||||
}) {
|
||||
export async function getNotificationHistory(
|
||||
filters?: {
|
||||
channelId?: string;
|
||||
policyId?: string;
|
||||
organizationId?: string;
|
||||
level?: NotificationLevel;
|
||||
success?: boolean;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
limit?: number;
|
||||
}
|
||||
): Promise<NotificationLogWithRelations[]> {
|
||||
const where = [];
|
||||
if (filters?.channelId) where.push(eq(notificationLog.channelId, filters.channelId));
|
||||
if (filters?.policyId) where.push(eq(notificationLog.policyId, filters.policyId));
|
||||
@@ -28,7 +49,7 @@ export async function getNotificationHistory(filters?: {
|
||||
if (filters?.from) where.push(gte(notificationLog.sentAt, filters.from));
|
||||
if (filters?.to) where.push(lte(notificationLog.sentAt, filters.to));
|
||||
|
||||
return await db
|
||||
const rows = await db
|
||||
.select({
|
||||
id: notificationLog.id,
|
||||
title: notificationLog.title,
|
||||
@@ -36,14 +57,19 @@ export async function getNotificationHistory(filters?: {
|
||||
success: notificationLog.success,
|
||||
error: notificationLog.error,
|
||||
sentAt: notificationLog.sentAt,
|
||||
payload: notificationLog.payload,
|
||||
content: {
|
||||
title: notificationLog.title,
|
||||
message: notificationLog.message,
|
||||
},
|
||||
channel: {
|
||||
id: notificationLog.id,
|
||||
id: notificationChannel.id,
|
||||
name: notificationChannel.name,
|
||||
provider: notificationChannel.provider,
|
||||
},
|
||||
policy: {
|
||||
id: alertPolicy.id,
|
||||
eventKind: alertPolicy.eventKind,
|
||||
eventKinds: alertPolicy.eventKinds,
|
||||
},
|
||||
})
|
||||
.from(notificationLog)
|
||||
@@ -52,4 +78,9 @@ export async function getNotificationHistory(filters?: {
|
||||
.where(and(...where))
|
||||
.orderBy(desc(notificationLog.sentAt))
|
||||
.limit(filters?.limit || 100);
|
||||
}
|
||||
|
||||
return rows.map(row => ({
|
||||
...row,
|
||||
payload: row.payload as Json,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user