From faaa4033bd4b71e50d8de8e88aeba48c5b53d2d9 Mon Sep 17 00:00:00 2001 From: charlesgauthereau Date: Mon, 12 Jan 2026 21:16:52 +0100 Subject: [PATCH] feat: backend storage --- .../(admin)/notifications/channels/page.tsx | 9 +- .../(admin)/storages/channels/page.tsx | 46 ++ .../(organization)/settings/page.tsx | 7 - .../wrappers/auth/auth-logo-section.tsx | 2 +- .../channel/channel-add-edit-modal.tsx} | 84 +-- .../channel-card/button-delete-channel.tsx} | 25 +- .../channel-card/button-edit-channel.tsx | 91 ++++ .../channel/channel-card/channel-card.tsx} | 56 +- .../channel-form/channel-form.schema.ts | 27 + .../channel/channel-form/channel-form.tsx} | 129 ++--- .../channel-form/channel-test-button.tsx | 76 +++ .../providers/notifications/action.ts} | 17 +- .../notifications/forms/discord.form.tsx} | 0 .../notifications/forms/gotify.form.tsx} | 0 .../notifications/forms/ntfy.form.tsx} | 0 .../notifications/forms/slack.form.tsx} | 0 .../notifications/forms/smtp.form.tsx} | 0 .../notifications/forms/telegram.form.tsx} | 0 .../notifications/forms/webhook.form.tsx} | 0 .../channel-form/providers/storages/action.ts | 172 +++++++ .../admin/channels/channels-section.tsx | 57 ++ .../admin/channels/helpers/common.tsx | 88 ++++ .../admin/channels/helpers/notification.tsx | 483 +++++++++++++++++ .../admin/channels/helpers/storage.tsx | 6 + .../channels-organization-form.tsx} | 44 +- .../channels-organization.action.ts | 154 ++++++ .../channels-organization.schema.ts | 7 + .../notification-channels-section.tsx | 50 -- ...tification-channels-organization.action.ts | 79 --- ...tification-channels-organization.schema.ts | 7 - .../dashboard/admin/notifications/helpers.tsx | 487 ------------------ .../admin/notifications/logs/columns.tsx | 4 +- .../notifier-card/button-edit-notifier.tsx | 72 --- .../notifier-form/notifier-form.schema.ts | 21 - .../notifier-test-channel-button.tsx | 61 --- .../common/sidebar/menu-sidebar-main.tsx | 12 +- .../organization-notifiers-tab.tsx | 17 +- src/db/index.ts | 4 +- src/db/migrations/meta/_journal.json | 7 + src/db/schema/03_organization.ts | 3 + src/db/schema/12_storage-channel.ts | 58 +++ src/features/storages/dispatch.ts | 129 +++++ 42 files changed, 1623 insertions(+), 968 deletions(-) create mode 100644 app/(customer)/dashboard/(admin)/storages/channels/page.tsx rename src/components/wrappers/dashboard/{common/notifier/notifier-add-edit-modal.tsx => admin/channels/channel/channel-add-edit-modal.tsx} (56%) rename src/components/wrappers/dashboard/{common/notifier/notifier-card/button-delete-notifier.tsx => admin/channels/channel/channel-card/button-delete-channel.tsx} (57%) create mode 100644 src/components/wrappers/dashboard/admin/channels/channel/channel-card/button-edit-channel.tsx rename src/components/wrappers/dashboard/{common/notifier/notifier-card/notifier-card.tsx => admin/channels/channel/channel-card/channel-card.tsx} (50%) create mode 100644 src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema.ts rename src/components/wrappers/dashboard/{common/notifier/notifier-form/notifier-form.tsx => admin/channels/channel/channel-form/channel-form.tsx} (65%) create mode 100644 src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button.tsx rename src/components/wrappers/dashboard/{common/notifier/notifier-form/notifier-form.action.ts => admin/channels/channel/channel-form/providers/notifications/action.ts} (94%) rename src/components/wrappers/dashboard/{common/notifier/notifier-form/providers/notifier-discord.form.tsx => admin/channels/channel/channel-form/providers/notifications/forms/discord.form.tsx} (100%) rename src/components/wrappers/dashboard/{common/notifier/notifier-form/providers/notifier-gotify.form.tsx => admin/channels/channel/channel-form/providers/notifications/forms/gotify.form.tsx} (100%) rename src/components/wrappers/dashboard/{common/notifier/notifier-form/providers/notifier-ntfy.form.tsx => admin/channels/channel/channel-form/providers/notifications/forms/ntfy.form.tsx} (100%) rename src/components/wrappers/dashboard/{common/notifier/notifier-form/providers/notifier-slack.form.tsx => admin/channels/channel/channel-form/providers/notifications/forms/slack.form.tsx} (100%) rename src/components/wrappers/dashboard/{common/notifier/notifier-form/providers/notifier-smtp.form.tsx => admin/channels/channel/channel-form/providers/notifications/forms/smtp.form.tsx} (100%) rename src/components/wrappers/dashboard/{common/notifier/notifier-form/providers/notifier-telegram.form.tsx => admin/channels/channel/channel-form/providers/notifications/forms/telegram.form.tsx} (100%) rename src/components/wrappers/dashboard/{common/notifier/notifier-form/providers/notifier-webhook.form.tsx => admin/channels/channel/channel-form/providers/notifications/forms/webhook.form.tsx} (100%) create mode 100644 src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/action.ts create mode 100644 src/components/wrappers/dashboard/admin/channels/channels-section.tsx create mode 100644 src/components/wrappers/dashboard/admin/channels/helpers/common.tsx create mode 100644 src/components/wrappers/dashboard/admin/channels/helpers/notification.tsx create mode 100644 src/components/wrappers/dashboard/admin/channels/helpers/storage.tsx rename src/components/wrappers/dashboard/admin/{notifications/channels/organization/notification-channels-organization-form.tsx => channels/organization/channels-organization-form.tsx} (66%) create mode 100644 src/components/wrappers/dashboard/admin/channels/organization/channels-organization.action.ts create mode 100644 src/components/wrappers/dashboard/admin/channels/organization/channels-organization.schema.ts delete mode 100644 src/components/wrappers/dashboard/admin/notifications/channels/notification-channels-section.tsx delete mode 100644 src/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.action.ts delete mode 100644 src/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.schema.ts delete mode 100644 src/components/wrappers/dashboard/admin/notifications/helpers.tsx delete mode 100644 src/components/wrappers/dashboard/common/notifier/notifier-card/button-edit-notifier.tsx delete mode 100644 src/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.schema.ts delete mode 100644 src/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button.tsx create mode 100644 src/db/schema/12_storage-channel.ts create mode 100644 src/features/storages/dispatch.ts diff --git a/app/(customer)/dashboard/(admin)/notifications/channels/page.tsx b/app/(customer)/dashboard/(admin)/notifications/channels/page.tsx index a4bf48d3..00f2e2e9 100644 --- a/app/(customer)/dashboard/(admin)/notifications/channels/page.tsx +++ b/app/(customer)/dashboard/(admin)/notifications/channels/page.tsx @@ -8,6 +8,8 @@ import {db} from "@/db"; import {notificationChannel, NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel"; import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal"; import {desc, isNull} from "drizzle-orm"; +import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section"; +import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal"; export const metadata: Metadata = { title: "Notification Channels", @@ -35,11 +37,14 @@ export default async function RoutePage(props: PageParams<{}>) { Notification channels - + {/**/} + - + {/**/} + ); diff --git a/app/(customer)/dashboard/(admin)/storages/channels/page.tsx b/app/(customer)/dashboard/(admin)/storages/channels/page.tsx new file mode 100644 index 00000000..898f1131 --- /dev/null +++ b/app/(customer)/dashboard/(admin)/storages/channels/page.tsx @@ -0,0 +1,46 @@ +import {PageParams} from "@/types/next"; +import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; +import {Metadata} from "next"; +import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal"; +import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section"; +import {db} from "@/db"; +import {desc, isNull} from "drizzle-orm"; +import * as drizzleDb from "@/db"; +import {StorageChannelWith} from "@/db/schema/12_storage-channel"; +import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal"; + +export const metadata: Metadata = { + title: "Storage Channels", +}; + +export default async function RoutePage(props: PageParams<{}>) { + + const storageChannels = await db.query.storageChannel.findMany({ + with: { + organizations: true + }, + orderBy: desc(drizzleDb.schemas.storageChannel.createdAt) + }) as StorageChannelWith[] + + const organizations = await db.query.organization.findMany({ + where: (fields) => isNull(fields.deletedAt), + with: { + members: true, + }, + }); + + + return ( + + + Storage channels + + + + + + + + + ); +} diff --git a/app/(customer)/dashboard/(organization)/settings/page.tsx b/app/(customer)/dashboard/(organization)/settings/page.tsx index 131e2d62..6974a927 100644 --- a/app/(customer)/dashboard/(organization)/settings/page.tsx +++ b/app/(customer)/dashboard/(organization)/settings/page.tsx @@ -11,13 +11,6 @@ import {Metadata} from "next"; import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs"; import {getOrganizationChannels} from "@/db/services/notification-channel"; import {computeOrganizationPermissions} from "@/lib/acl/organization-acl"; -import {capitalizeFirstLetter} from "@/utils/text"; -import Link from "next/link"; -import {buttonVariants} from "@/components/ui/button"; -import {GearIcon} from "@radix-ui/react-icons"; -import { - ButtonDeleteProject -} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project"; export const metadata: Metadata = { title: "Settings", diff --git a/src/components/wrappers/auth/auth-logo-section.tsx b/src/components/wrappers/auth/auth-logo-section.tsx index 81d1a0e5..11d3d30c 100644 --- a/src/components/wrappers/auth/auth-logo-section.tsx +++ b/src/components/wrappers/auth/auth-logo-section.tsx @@ -41,7 +41,7 @@ export const AuthLogoSection = () => { /> )} - + v{env.NEXT_PUBLIC_PROJECT_VERSION} diff --git a/src/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal.tsx similarity index 56% rename from src/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal.tsx rename to src/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal.tsx index d0033ad3..8e63dcb8 100644 --- a/src/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal.tsx +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal.tsx @@ -6,44 +6,51 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; 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, NotificationChannelWith} from "@/db/schema/09_notification-channel"; +import {NotificationChannelWith} from "@/db/schema/09_notification-channel"; import {useIsMobile} from "@/hooks/use-mobile"; import {useEffect, useState} from "react"; import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"; +import {StorageChannelWith} from "@/db/schema/12_storage-channel"; +import {ChannelForm} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form"; +import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common"; import { - NotifierChannelOrganisationForm -} from "@/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization-form"; + ChannelOrganisationForm +} from "@/components/wrappers/dashboard/admin/channels/organization/channels-organization-form"; -type OrganizationNotifierAddModalProps = { - notificationChannel?: NotificationChannelWith +type ChannelAddModalProps = { + channel?: NotificationChannelWith | StorageChannelWith organization?: OrganizationWithMembers; open?: boolean; onOpenChangeAction?: (open: boolean) => void; adminView?: boolean; organizations?: OrganizationWithMembers[] trigger?: boolean; + kind: ChannelKind; } -export const NotifierAddEditModal = ({ - organization, - notificationChannel, - open = false, - onOpenChangeAction, - adminView, - organizations , - trigger= true - }: OrganizationNotifierAddModalProps) => { +export const ChannelAddEditModal = ({ + organization, + channel, + open = false, + onOpenChangeAction, + adminView, + organizations, + trigger = true, + kind + }: ChannelAddModalProps) => { const isMobile = useIsMobile(); const [openInternal, setOpen] = useState(open); - const isCreate = !Boolean(notificationChannel); + const isCreate = !Boolean(channel); useEffect(() => { setOpen(open); - },[open]) + }, [open]) + + + const channelText = getChannelTextBasedOnKind(kind) return ( @@ -55,7 +62,7 @@ export const NotifierAddEditModal = ({ {isCreate ? : + ) +} \ No newline at end of file diff --git a/src/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.action.ts b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/action.ts similarity index 94% rename from src/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.action.ts rename to src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/action.ts index 5f773f4f..e47d60ad 100644 --- a/src/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.action.ts +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/action.ts @@ -6,11 +6,11 @@ import * as drizzleDb from "@/db"; import {userAction} from "@/lib/safe-actions/actions"; import {NotificationChannel} from "@/db/schema/09_notification-channel"; import {db} from "@/db"; -import { - NotificationChannelFormSchema -} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.schema"; import {and, eq} from "drizzle-orm"; import {withUpdatedAt} from "@/db/utils"; +import { + NotificationChannelFormSchema +} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema"; export const addNotificationChannelAction = userAction.schema( @@ -20,7 +20,6 @@ export const addNotificationChannelAction = userAction.schema( }) ).action(async ({parsedInput}): Promise> => { const {organizationId, data} = parsedInput; - try { const [channel] = await db .insert(drizzleDb.schemas.notificationChannel) @@ -128,11 +127,11 @@ export const removeNotificationChannelAction = userAction.schema( export const updateNotificationChannelAction = userAction.schema( z.object({ - notificationChannelId: z.string(), + id: z.string(), data: NotificationChannelFormSchema }) ).action(async ({parsedInput}): Promise> => { - const {notificationChannelId, data} = parsedInput; + const {id, data} = parsedInput; try { const [channel] = await db @@ -143,7 +142,7 @@ export const updateNotificationChannelAction = userAction.schema( config: data.config, enabled: data.enabled ?? true, })) - .where(eq(drizzleDb.schemas.notificationChannel.id, notificationChannelId)) + .where(eq(drizzleDb.schemas.notificationChannel.id, id)) .returning(); return { @@ -154,7 +153,7 @@ export const updateNotificationChannelAction = userAction.schema( }, actionSuccess: { message: `Notification channel "${channel.name}" has been successfully updated.`, - messageParams: {notificationChannelId: channel.id}, + messageParams: {id: channel.id}, }, }; } catch (error) { @@ -165,7 +164,7 @@ export const updateNotificationChannelAction = userAction.schema( message: "Failed to update notification channel.", status: 500, cause: error instanceof Error ? error.message : "Unknown error", - messageParams: {notificationChannelId: ""}, + messageParams: {id: ""}, }, }; } diff --git a/src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-discord.form.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/discord.form.tsx similarity index 100% rename from src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-discord.form.tsx rename to src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/discord.form.tsx diff --git a/src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-gotify.form.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/gotify.form.tsx similarity index 100% rename from src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-gotify.form.tsx rename to src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/gotify.form.tsx diff --git a/src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-ntfy.form.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/ntfy.form.tsx similarity index 100% rename from src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-ntfy.form.tsx rename to src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/ntfy.form.tsx diff --git a/src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-slack.form.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/slack.form.tsx similarity index 100% rename from src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-slack.form.tsx rename to src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/slack.form.tsx diff --git a/src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-smtp.form.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/smtp.form.tsx similarity index 100% rename from src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-smtp.form.tsx rename to src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/smtp.form.tsx diff --git a/src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-telegram.form.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/telegram.form.tsx similarity index 100% rename from src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-telegram.form.tsx rename to src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/telegram.form.tsx diff --git a/src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-webhook.form.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/webhook.form.tsx similarity index 100% rename from src/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-webhook.form.tsx rename to src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/webhook.form.tsx diff --git a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/action.ts b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/action.ts new file mode 100644 index 00000000..8c19df3a --- /dev/null +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/action.ts @@ -0,0 +1,172 @@ +"use server"; + +import {z} from "zod"; +import {ServerActionResult} from "@/types/action-type"; +import * as drizzleDb from "@/db"; +import {userAction} from "@/lib/safe-actions/actions"; +import {db} from "@/db"; +import {and, eq} from "drizzle-orm"; +import {withUpdatedAt} from "@/db/utils"; +import { + StorageChannelFormSchema +} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema"; +import {StorageChannel} from "@/db/schema/12_storage-channel"; + + +export const addStorageChannelAction = userAction.schema( + z.object({ + organizationId: z.string().optional(), + data: StorageChannelFormSchema + }) +).action(async ({parsedInput}): Promise> => { + const {organizationId, data} = parsedInput; + + try { + const [channel] = await db + .insert(drizzleDb.schemas.storageChannel) + .values({ + provider: data.provider, + name: data.name, + config: data.config, + enabled: data.enabled ?? true, + }) + .returning(); + + if (organizationId) { + await db.insert(drizzleDb.schemas.organizationStorageChannel).values({ + organizationId, + storageChannelId: channel.id, + }); + } + + return { + success: true, + value: { + ...channel, + config: channel.config as JSON + }, + actionSuccess: { + message: "Storage channel has been successfully created.", + messageParams: {id: channel.id}, + }, + }; + } catch (error) { + console.error("Error:", error); + return { + success: false, + actionError: { + message: "Failed to create storage channel.", + status: 500, + cause: error instanceof Error ? error.message : "Unknown error", + messageParams: {id: ""}, + }, + }; + } +}); + +export const removeStorageChannelAction = userAction.schema( + z.object({ + organizationId: z.string().optional(), + id: z.string(), + }) +).action(async ({parsedInput}): Promise> => { + const {organizationId, id} = parsedInput; + + try { + if (organizationId) { + await db + .delete(drizzleDb.schemas.organizationNotificationChannel) + .where( + and( + eq(drizzleDb.schemas.organizationStorageChannel.organizationId, organizationId), + eq(drizzleDb.schemas.organizationStorageChannel.storageChannelId, id) + ) + ); + } + + const [deletedChannel] = await db + .delete(drizzleDb.schemas.storageChannel) + .where(eq(drizzleDb.schemas.storageChannel.id, id)) + .returning(); + + if (!deletedChannel) { + return { + success: false, + actionError: { + message: "Storage channel not found.", + status: 404, + messageParams: {id: id}, + }, + }; + } + + return { + success: true, + value: { + ...deletedChannel, + config: deletedChannel.config as JSON + }, + actionSuccess: { + message: "Storage channel has been successfully removed.", + messageParams: {id: id}, + }, + }; + } catch (error) { + console.error("Error:", error); + return { + success: false, + actionError: { + message: "Failed to remove storage channel.", + status: 500, + cause: error instanceof Error ? error.message : "Unknown error", + messageParams: {id: id}, + }, + }; + } +}); + + +export const updateStorageChannelAction = userAction.schema( + z.object({ + id: z.string(), + data: StorageChannelFormSchema + }) +).action(async ({parsedInput}): Promise> => { + const {id, data} = parsedInput; + + try { + const [channel] = await db + .update(drizzleDb.schemas.storageChannel) + .set(withUpdatedAt({ + provider: data.provider, + name: data.name, + config: data.config, + enabled: data.enabled ?? true, + })) + .where(eq(drizzleDb.schemas.storageChannel.id, id)) + .returning(); + + return { + success: true, + value: { + ...channel, + config: channel.config as JSON + }, + actionSuccess: { + message: `Storage channel "${channel.name}" has been successfully updated.`, + messageParams: {id: channel.id}, + }, + }; + } catch (error) { + console.error("Error:", error); + return { + success: false, + actionError: { + message: "Failed to update storage channel.", + status: 500, + cause: error instanceof Error ? error.message : "Unknown error", + messageParams: {id: ""}, + }, + }; + } +}); diff --git a/src/components/wrappers/dashboard/admin/channels/channels-section.tsx b/src/components/wrappers/dashboard/admin/channels/channels-section.tsx new file mode 100644 index 00000000..cf1adfb2 --- /dev/null +++ b/src/components/wrappers/dashboard/admin/channels/channels-section.tsx @@ -0,0 +1,57 @@ +"use client" +import {NotificationChannelWith} from "@/db/schema/09_notification-channel"; +import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; +import {useState} from "react"; +import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder"; +import {OrganizationWithMembers} from "@/db/schema/03_organization"; +import {StorageChannelWith} from "@/db/schema/12_storage-channel"; +import {ChannelCard} from "@/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card"; +import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal"; +import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common"; + +type ChannelsSectionProps = { + channels: NotificationChannelWith[] | StorageChannelWith[] + organizations: OrganizationWithMembers[] + kind: ChannelKind; +} + +export const ChannelsSection = ({ + organizations, + channels, + kind + }: ChannelsSectionProps) => { + + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const channelText = getChannelTextBasedOnKind(kind) + const hasChannels = channels.length > 0 + + + return ( +
+ + {hasChannels ? ( +
+ +
+ ) : ( + { + setIsAddModalOpen(true) + }} + className="h-full" + /> + )} +
+ ); +} \ No newline at end of file diff --git a/src/components/wrappers/dashboard/admin/channels/helpers/common.tsx b/src/components/wrappers/dashboard/admin/channels/helpers/common.tsx new file mode 100644 index 00000000..e1f607a2 --- /dev/null +++ b/src/components/wrappers/dashboard/admin/channels/helpers/common.tsx @@ -0,0 +1,88 @@ +import {UseFormReturn} from "react-hook-form"; +import { + NotifierSmtpForm +} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/smtp.form"; +import { + NotifierSlackForm +} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/slack.form"; +import { + NotifierDiscordForm +} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/discord.form"; +import { + NotifierTelegramForm +} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/telegram.form"; +import { + NotifierGotifyForm +} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/gotify.form"; +import { + NotifierNtfyForm +} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/ntfy.form"; +import { + NotifierWebhookForm +} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/webhook.form"; +import {notificationTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/notification"; +import {storageTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/storage"; +import {OrganizationWithMembers} from "@/db/schema/03_organization"; +import {NotificationChannelWith} from "@/db/schema/09_notification-channel"; +import {StorageChannelWith} from "@/db/schema/12_storage-channel"; +import {ForwardRefExoticComponent, JSX, RefAttributes, SVGProps} from "react"; +import {LucideProps} from "lucide-react"; + +export type ChannelKind = "notification" | "storage"; + +export function getChannelTextBasedOnKind(kind: ChannelKind) { + switch (kind) { + case "notification": + return "Notification"; + case "storage": + return "Storage"; + default: + return "Notification"; + } +} + + +type ProviderIconTypes = { + value: string + label: string + icon: ForwardRefExoticComponent & RefAttributes> +} | { + value: string + label: string + icon: (props: SVGProps) => JSX.Element +} + +const providerIcons: ProviderIconTypes[] = [ + ...notificationTypes, + ...storageTypes, +]; + + +export const getChannelIcon = (type: string) => { + const Icon = providerIcons.find((t) => t.value === type)?.icon + return Icon ? : null +} + + +export const renderChannelForm = (provider: string | undefined, form: UseFormReturn) => { + switch (provider) { + case "smtp": + return ; + case "slack": + return ; + case "discord": + return ; + case "telegram": + return ; + case "gotify": + return ; + case "ntfy": + return ; + case "webhook": + return ; + case "local": + return <> + default: + return null; + } +}; \ No newline at end of file diff --git a/src/components/wrappers/dashboard/admin/channels/helpers/notification.tsx b/src/components/wrappers/dashboard/admin/channels/helpers/notification.tsx new file mode 100644 index 00000000..460832d8 --- /dev/null +++ b/src/components/wrappers/dashboard/admin/channels/helpers/notification.tsx @@ -0,0 +1,483 @@ +import {Mail} from "lucide-react"; +import type { SVGProps } from 'react'; + + + +export const notificationTypes = [ + {value: "smtp", label: "Email", icon: Mail}, + {value: "slack", label: "Slack", icon: SlackIcon}, + {value: "discord", label: "Discord", icon: DiscordIcon}, + {value: "telegram", label: "Telegram", icon: TelegramIcon}, + {value: "gotify", label: "Gotify", icon: GotifyIcon}, + {value: "ntfy", label: "ntfy.sh", icon: NtfyIcon}, + {value: "webhook", label: "Webhook", icon: WebhookIcon}, +] + + +export function SlackIcon(props: SVGProps) { + return (); +} + +export function DiscordIcon(props: SVGProps) { + return (); +} + +export function TelegramIcon(props: SVGProps) { + return (); +} + + +export function NtfyIcon(props: SVGProps) { + + return ( + + + + + + + + + + + ) + +} + + +export function GotifyIcon(props: SVGProps) { + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export function WebhookIcon(props: SVGProps) { + return ( + + + + ) +} + diff --git a/src/components/wrappers/dashboard/admin/channels/helpers/storage.tsx b/src/components/wrappers/dashboard/admin/channels/helpers/storage.tsx new file mode 100644 index 00000000..f1d05f92 --- /dev/null +++ b/src/components/wrappers/dashboard/admin/channels/helpers/storage.tsx @@ -0,0 +1,6 @@ +import {Server} from "lucide-react"; + +export const storageTypes = [ + {value: "local", label: "Local", icon: Server}, + {value: "s3", label: "s3", icon: Server}, +] \ No newline at end of file diff --git a/src/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization-form.tsx b/src/components/wrappers/dashboard/admin/channels/organization/channels-organization-form.tsx similarity index 66% rename from src/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization-form.tsx rename to src/components/wrappers/dashboard/admin/channels/organization/channels-organization-form.tsx index e413171a..3f7a5b80 100644 --- a/src/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization-form.tsx +++ b/src/components/wrappers/dashboard/admin/channels/organization/channels-organization-form.tsx @@ -6,26 +6,30 @@ import {Form, FormControl, FormField, FormItem, useZodForm} from "@/components/u 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"; +import {StorageChannelWith} from "@/db/schema/12_storage-channel"; +import { + ChannelsOrganizationSchema, ChannelsOrganizationType +} from "@/components/wrappers/dashboard/admin/channels/organization/channels-organization.schema"; +import { + updateNotificationChannelsOrganizationAction, updateStorageChannelsOrganizationAction +} from "@/components/wrappers/dashboard/admin/channels/organization/channels-organization.action"; +import {ChannelKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common"; -type NotifierChannelOrganisationFormProps = { +type ChannelOrganisationFormProps = { organizations?: OrganizationWithMembers[]; - defaultValues?: NotificationChannelWith + defaultValues?: NotificationChannelWith | StorageChannelWith + kind: ChannelKind }; -export const NotifierChannelOrganisationForm = ({ - organizations, - defaultValues, - }: NotifierChannelOrganisationFormProps) => { +export const ChannelOrganisationForm = ({ + organizations, + defaultValues, + kind + }: ChannelOrganisationFormProps) => { const router = useRouter(); @@ -33,7 +37,7 @@ export const NotifierChannelOrganisationForm = ({ const form = useZodForm({ - schema: NotificationChannelsOrganizationSchema, + schema: ChannelsOrganizationSchema, // @ts-ignore defaultValues: { organizations: defaultOrganizationIds @@ -49,15 +53,15 @@ export const NotifierChannelOrganisationForm = ({ }; - const mutationUpdateNotificationChannelOrganizations = useMutation({ - mutationFn: async (values: NotificationChannelsOrganizationType) => { + const mutationUpdateChannelOrganizations = useMutation({ + mutationFn: async (values: ChannelsOrganizationType) => { const payload = { data: values.organizations, - notificationChannelId: defaultValues?.id ?? "" + id: defaultValues?.id ?? "" }; - const result = await updateNotificationChannelsOrganizationAction(payload) + const result = kind === "notification" ? await updateNotificationChannelsOrganizationAction(payload) : await updateStorageChannelsOrganizationAction(payload) const inner = result?.data; if (inner?.success) { @@ -76,7 +80,7 @@ export const NotifierChannelOrganisationForm = ({ form={form} className="flex flex-col gap-4" onSubmit={async (values) => { - await mutationUpdateNotificationChannelOrganizations.mutateAsync(values); + await mutationUpdateChannelOrganizations.mutateAsync(values); }} >
- + Save
diff --git a/src/components/wrappers/dashboard/admin/channels/organization/channels-organization.action.ts b/src/components/wrappers/dashboard/admin/channels/organization/channels-organization.action.ts new file mode 100644 index 00000000..c8481e93 --- /dev/null +++ b/src/components/wrappers/dashboard/admin/channels/organization/channels-organization.action.ts @@ -0,0 +1,154 @@ +"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"; +import {StorageChannelWith} from "@/db/schema/12_storage-channel"; + + +export const updateNotificationChannelsOrganizationAction = userAction + .schema( + z.object({ + data: z.array(z.string()), + id: z.string(), + }) + ) + .action(async ({parsedInput , ctx}): Promise> => { + try { + const organizationsIds = parsedInput.data; + const notificationChannelId = parsedInput.id; + + const notificationChannel = await db.query.notificationChannel.findFirst({ + where: eq(drizzleDb.schemas.notificationChannel.id, 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: notificationChannelId + }); + } + } + if (organizationsToRemove.length > 0) { + await db.delete(drizzleDb.schemas.organizationNotificationChannel).where(and(inArray(drizzleDb.schemas.organizationNotificationChannel.organizationId, organizationsToRemove), eq(drizzleDb.schemas.organizationNotificationChannel.notificationChannelId,notificationChannelId))).execute(); + + } + + return { + success: true, + value: null, + actionSuccess: { + message: "Notification channel organizations has been successfully updated.", + messageParams: {notificationChannelId: 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"}, + }, + }; + } + }); + + +export const updateStorageChannelsOrganizationAction = userAction + .schema( + z.object({ + data: z.array(z.string()), + id: z.string(), + }) + ) + .action(async ({parsedInput, ctx}): Promise> => { + try { + const organizationsIds = parsedInput.data; + const storageChannelId = parsedInput.id; + + const storageChannel = await db.query.storageChannel.findFirst({ + where: eq(drizzleDb.schemas.storageChannel.id, storageChannelId), + with: { + organizations: true, + } + }) as StorageChannelWith; + + + if (!storageChannel) { + return { + success: false, + actionError: { + message: "Storage channel not found.", + status: 404, + cause: "not_found", + }, + }; + } + + const existingItemIds = storageChannel.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.organizationStorageChannel).values({ + organizationId: organizationToAdd, + storageChannelId: storageChannelId + }); + } + } + + if (organizationsToRemove.length > 0) { + await db.delete(drizzleDb.schemas.organizationStorageChannel).where(and(inArray(drizzleDb.schemas.organizationStorageChannel.organizationId, organizationsToRemove), eq(drizzleDb.schemas.organizationStorageChannel.storageChannelId, storageChannelId))).execute(); + + } + + return { + success: true, + value: null, + actionSuccess: { + message: "Storage channel organizations has been successfully updated.", + messageParams: {storageChannelId: storageChannelId}, + }, + }; + } catch (error) { + console.error("Error updating storage channel:", error); + return { + success: false, + actionError: { + message: "Failed to update storage channel.", + status: 500, + cause: "server_error", + messageParams: {message: "Error updating the storage channel"}, + }, + }; + } + }); diff --git a/src/components/wrappers/dashboard/admin/channels/organization/channels-organization.schema.ts b/src/components/wrappers/dashboard/admin/channels/organization/channels-organization.schema.ts new file mode 100644 index 00000000..3146c5d7 --- /dev/null +++ b/src/components/wrappers/dashboard/admin/channels/organization/channels-organization.schema.ts @@ -0,0 +1,7 @@ +import {z} from "zod"; + +export const ChannelsOrganizationSchema = z.object({ + organizations: z.array(z.string()) +}); + +export type ChannelsOrganizationType = z.infer; diff --git a/src/components/wrappers/dashboard/admin/notifications/channels/notification-channels-section.tsx b/src/components/wrappers/dashboard/admin/notifications/channels/notification-channels-section.tsx deleted file mode 100644 index f3d6208c..00000000 --- a/src/components/wrappers/dashboard/admin/notifications/channels/notification-channels-section.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"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"; -import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal"; - -type NotificationChannelsSectionProps = { - notificationChannels: NotificationChannelWith[] - organizations: OrganizationWithMembers[] -} - -export const NotificationChannelsSection = ({ - organizations, - notificationChannels - }: NotificationChannelsSectionProps) => { - - const [isAddModalOpen, setIsAddModalOpen] = useState(false); - - const hasNotifiers = notificationChannels.length > 0; - - return ( -
- - {hasNotifiers ? ( -
- - -
- ) : ( - { - setIsAddModalOpen(true) - }} - className="h-full" - /> - )} -
- ); -} \ No newline at end of file diff --git a/src/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.action.ts b/src/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.action.ts deleted file mode 100644 index 395c685e..00000000 --- a/src/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.action.ts +++ /dev/null @@ -1,79 +0,0 @@ -"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> => { - 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"}, - }, - }; - } - }); diff --git a/src/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.schema.ts b/src/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.schema.ts deleted file mode 100644 index efc13c62..00000000 --- a/src/components/wrappers/dashboard/admin/notifications/channels/organization/notification-channels-organization.schema.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {z} from "zod"; - -export const NotificationChannelsOrganizationSchema = z.object({ - organizations: z.array(z.string()) -}); - -export type NotificationChannelsOrganizationType = z.infer; diff --git a/src/components/wrappers/dashboard/admin/notifications/helpers.tsx b/src/components/wrappers/dashboard/admin/notifications/helpers.tsx deleted file mode 100644 index cdaea432..00000000 --- a/src/components/wrappers/dashboard/admin/notifications/helpers.tsx +++ /dev/null @@ -1,487 +0,0 @@ -import {Mail} from "lucide-react"; -import type { SVGProps } from 'react'; - -export const getNotificationChannelIcon = (type: string) => { - const Icon = notificationTypes.find((t) => t.value === type)?.icon - return Icon ? : null -} - - -export const notificationTypes = [ - {value: "smtp", label: "Email", icon: Mail}, - {value: "slack", label: "Slack", icon: SlackIcon}, - {value: "discord", label: "Discord", icon: DiscordIcon}, - {value: "telegram", label: "Telegram", icon: TelegramIcon}, - {value: "gotify", label: "Gotify", icon: GotifyIcon}, - {value: "ntfy", label: "ntfy.sh", icon: NtfyIcon}, - {value: "webhook", label: "Webhook", icon: WebhookIcon}, -] - - -export function SlackIcon(props: SVGProps) { - return (); -} - -export function DiscordIcon(props: SVGProps) { - return (); -} - -export function TelegramIcon(props: SVGProps) { - return (); -} - - -export function NtfyIcon(props: SVGProps) { - - return ( - - - - - - - - - - - ) - -} - - -export function GotifyIcon(props: SVGProps) { - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} - -export function WebhookIcon(props: SVGProps) { - return ( - - - - ) -} - diff --git a/src/components/wrappers/dashboard/admin/notifications/logs/columns.tsx b/src/components/wrappers/dashboard/admin/notifications/logs/columns.tsx index 3ae7de36..4f44b7cd 100644 --- a/src/components/wrappers/dashboard/admin/notifications/logs/columns.tsx +++ b/src/components/wrappers/dashboard/admin/notifications/logs/columns.tsx @@ -2,11 +2,11 @@ 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"; +import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common"; export function notificationLogsColumns(): ColumnDef[] { @@ -33,7 +33,7 @@ export function notificationLogsColumns(): ColumnDef
- {getNotificationChannelIcon(channel?.provider ?? "")} + {getChannelIcon(channel?.provider ?? "")}
{channel?.name} diff --git a/src/components/wrappers/dashboard/common/notifier/notifier-card/button-edit-notifier.tsx b/src/components/wrappers/dashboard/common/notifier/notifier-card/button-edit-notifier.tsx deleted file mode 100644 index b8bff033..00000000 --- a/src/components/wrappers/dashboard/common/notifier/notifier-card/button-edit-notifier.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; -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, NotificationChannelWith} from "@/db/schema/09_notification-channel"; -import {Switch} from "@/components/ui/switch"; -import { - updateNotificationChannelAction -} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.action"; -import {toast} from "sonner"; -import {useState} from "react"; -import {Organization, OrganizationWithMembers} from "@/db/schema/03_organization"; - -export type EditNotifierButtonProps = { - notificationChannel: NotificationChannelWith; - organization?: OrganizationWithMembers; - organizations?: OrganizationWithMembers[]; - adminView?: boolean; -}; - -export const EditNotifierButton = ({ - organizations, - adminView = false, - notificationChannel, - organization - }: EditNotifierButtonProps) => { - const router = useRouter(); - const [isAddModalOpen, setIsAddModalOpen] = useState(false); - - - const mutation = useMutation({ - mutationFn: async (value: boolean) => { - - const payload = { - data: { - name: notificationChannel.name, - provider: notificationChannel.provider, - config: notificationChannel.config as Record, - enabled: value - }, - notificationChannelId: notificationChannel.id - }; - - const result = await updateNotificationChannelAction(payload); - const inner = result?.data; - - if (inner?.success) { - toast.success(inner.actionSuccess?.message); - router.refresh(); - } else { - toast.error(inner?.actionError?.message); - } - }, - }); - - return ( - <> - { - await mutation.mutateAsync(!notificationChannel.enabled) - }} - /> - - - ); -}; diff --git a/src/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.schema.ts b/src/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.schema.ts deleted file mode 100644 index d5988ca1..00000000 --- a/src/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.schema.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {z} from "zod"; - - -export const NotificationChannelFormSchema = z.object({ - name: z - .string() - .min(5, "Name must be at least 5 characters long") - .max(40, "Name must be at most 40 characters long"), - - provider: z.enum(["slack", "smtp", "discord", "telegram", "gotify", "ntfy", "webhook"], { - required_error: "Provider is required", - }), - - config: z.record(z.union([z.string(), z.number(), z.boolean(), z.null(), z.undefined()])).optional(), - - - enabled: z.boolean().default(true), - -}); - -export type NotificationChannelFormType = z.infer; diff --git a/src/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button.tsx b/src/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button.tsx deleted file mode 100644 index 0070eb0f..00000000 --- a/src/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client" -import {Button} from "@/components/ui/button"; -import {NotificationChannel} from "@/db/schema/09_notification-channel"; -import {useMutation} from "@tanstack/react-query"; -import {dispatchNotification} from "@/features/notifications/dispatch"; -import {EventPayload} from "@/features/notifications/types"; -import {Send} from "lucide-react"; -import {toast} from "sonner"; -import {useIsMobile} from "@/hooks/use-mobile"; -import {cn} from "@/lib/utils"; - -type NotifierTestChannelButtonProps = { - notificationChannel: NotificationChannel; - organizationId?: string; -} - -export const NotifierTestChannelButton = ({notificationChannel, organizationId}: NotifierTestChannelButtonProps) => { - - const isMobile = useIsMobile() - const mutation = useMutation({ - mutationFn: async () => { - - const payload: EventPayload = { - title: 'Test Channel', - message: `We are testing channel ${notificationChannel.name}`, - level: 'info', - // data: {host: 'db-prod-01', error: 'connection timeout'}, - }; - - const result = await dispatchNotification(payload, undefined, notificationChannel.id, organizationId); - - if (result.success) { - toast.success(result.message); - } else { - toast.error("An error occurred while testing the notification channel"); - } - }, - }); - - - return ( - - ) -} \ No newline at end of file diff --git a/src/components/wrappers/dashboard/common/sidebar/menu-sidebar-main.tsx b/src/components/wrappers/dashboard/common/sidebar/menu-sidebar-main.tsx index 6e21a381..be9f7ed4 100644 --- a/src/components/wrappers/dashboard/common/sidebar/menu-sidebar-main.tsx +++ b/src/components/wrappers/dashboard/common/sidebar/menu-sidebar-main.tsx @@ -7,7 +7,7 @@ import { Layers, ChartArea, ShieldHalf, - Building, UserRoundCog, Mail, PackageOpen, Logs, Megaphone, Blocks + Building, UserRoundCog, Mail, PackageOpen, Logs, Megaphone, Blocks, Warehouse } from "lucide-react"; import {SidebarGroupItem, SidebarMenuCustomBase} from "@/components/wrappers/dashboard/common/sidebar/menu-sidebar"; import {authClient, useSession} from "@/lib/auth/auth-client"; @@ -80,6 +80,16 @@ export const SidebarMenuCustomMain = () => { { title: "Activity Logs", url: "/notifications/logs", icon: Logs, type: "item" }, ], }, + { + title: "Storages", + url: "/storages", + icon: Warehouse, + details: true, + type: "collapse", + submenu: [ + { title: "Channels", url: "/storages/channels", icon: Blocks, type: "item" }, + ], + }, { title: "Access management", url: "/admin", diff --git a/src/components/wrappers/dashboard/organization/tabs/organization-notifiers-tab/organization-notifiers-tab.tsx b/src/components/wrappers/dashboard/organization/tabs/organization-notifiers-tab/organization-notifiers-tab.tsx index b5654046..db1e3bf2 100644 --- a/src/components/wrappers/dashboard/organization/tabs/organization-notifiers-tab/organization-notifiers-tab.tsx +++ b/src/components/wrappers/dashboard/organization/tabs/organization-notifiers-tab/organization-notifiers-tab.tsx @@ -1,11 +1,11 @@ import {OrganizationWithMembers} from "@/db/schema/03_organization"; import {NotificationChannel} 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 {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal"; import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder"; import {useState} from "react"; import {cn} from "@/lib/utils"; +import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal"; +import {ChannelCard} from "@/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card"; export type OrganizationNotifiersTabProps = { organization: OrganizationWithMembers; @@ -19,7 +19,7 @@ export const OrganizationNotifiersTab = ({ const [isAddModalOpen, setIsAddModalOpen] = useState(false); const hasNotifiers = notificationChannels.length > 0; - + const kind="notification" return (
@@ -29,7 +29,13 @@ export const OrganizationNotifiersTab = ({ Notification Settings
- */} +
) : ( diff --git a/src/db/index.ts b/src/db/index.ts index 2715ab6e..9e69f957 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -12,6 +12,7 @@ import * as notificationChannel from "./schema/09_notification-channel"; import * as organizationNotificationChannel from "./schema/09_notification-channel"; import * as alertPolicy from "./schema/10_alert-policy"; import * as notificationLog from "./schema/11_notification-log"; +import * as storageChannel from "./schema/12_storage-channel"; import {Pool} from "pg"; @@ -40,7 +41,8 @@ export const schemas = { ...notificationChannel, ...organizationNotificationChannel, ...alertPolicy, - ...notificationLog + ...notificationLog, + ...storageChannel }; export const db = drizzle({ diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 8a1d250a..e47cf140 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -141,6 +141,13 @@ "when": 1767782077775, "tag": "0019_overjoyed_butterfly", "breakpoints": true + }, + { + "idx": 20, + "version": "7", + "when": 1768248015695, + "tag": "0020_thankful_sunspot", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/03_organization.ts b/src/db/schema/03_organization.ts index cdca046f..6772e6bc 100644 --- a/src/db/schema/03_organization.ts +++ b/src/db/schema/03_organization.ts @@ -8,6 +8,7 @@ import {member, OrganizationMember} from "@/db/schema/04_member"; import {User} from "@/db/schema/02_user"; import {timestamps} from "@/db/schema/00_common"; import {NotificationChannel, organizationNotificationChannel} from "@/db/schema/09_notification-channel"; +import {organizationStorageChannel, StorageChannel} from "@/db/schema/12_storage-channel"; export const organization = pgTable("organization", { id: uuid("id").defaultRandom().primaryKey(), @@ -24,6 +25,7 @@ export const organizationRelations = relations(organization, ({many}) => ({ invitations: many(invitation), projects: many(project), notificationChannels: many(organizationNotificationChannel), + storageChannels: many(organizationStorageChannel), })); @@ -48,5 +50,6 @@ export type OrganizationWith = Organization & { members?: MemberWithUser[] | null; invitations?: OrganizationInvitation[] | null; notificationChannels?: NotificationChannel[] | null; + storageChannels?: StorageChannel[] | null; projects?: Project[] | null; }; diff --git a/src/db/schema/12_storage-channel.ts b/src/db/schema/12_storage-channel.ts new file mode 100644 index 00000000..1ca971b7 --- /dev/null +++ b/src/db/schema/12_storage-channel.ts @@ -0,0 +1,58 @@ +import {boolean, jsonb, pgEnum, pgTable, unique, uuid, varchar} from "drizzle-orm/pg-core"; +import {timestamps} from "@/db/schema/00_common"; +import {organization} from "@/db/schema/03_organization"; +import {relations} from "drizzle-orm"; +import {createSelectSchema} from "drizzle-zod"; +import {z} from "zod"; + + +export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3']); + +export const storageChannel = pgTable('storage_channel', { + id: uuid("id").defaultRandom().primaryKey(), + provider: providerStorageKindEnum('provider').notNull(), + name: varchar('name', {length: 255}).notNull(), + config: jsonb('config').notNull(), + enabled: boolean('enabled').default(false).notNull(), + ...timestamps +}); + +export const organizationStorageChannel = pgTable( + "organization_storage_channels", + { + organizationId: uuid('organization_id') + .notNull() + .references(() => organization.id, {onDelete: 'cascade'}), + storageChannelId: uuid('storage_channel_id') + .notNull() + .references(() => storageChannel.id, {onDelete: 'cascade'}), + }, + (t) => [unique().on(t.organizationId, t.storageChannelId)] +); + + +export const storageChannelRelations = relations(storageChannel, ({many}) => ({ + organizations: many(organizationStorageChannel), +})); + +export const organizationStorageChannelRelations = relations(organizationStorageChannel, ({one}) => ({ + organization: one(organization, { + fields: [organizationStorageChannel.organizationId], + references: [organization.id], + }), + storageChannel: one(storageChannel, { + fields: [organizationStorageChannel.storageChannelId], + references: [storageChannel.id], + }), +})); + +export const storageChannelSchema = createSelectSchema(storageChannel); +export type StorageChannel = z.infer; + + +export type StorageChannelWith = StorageChannel & { + organizations: { + organizationId: string; + storageChannelId: string; + }[]; +}; \ No newline at end of file diff --git a/src/features/storages/dispatch.ts b/src/features/storages/dispatch.ts new file mode 100644 index 00000000..49e027be --- /dev/null +++ b/src/features/storages/dispatch.ts @@ -0,0 +1,129 @@ +"use server"; +import {eq} from "drizzle-orm"; +import {dispatchViaProvider} from "./providers"; +import type {EventPayload, DispatchResult, EventKind} from "./types"; +import * as drizzleDb from "@/db"; +import {db} from "@/db"; +import {notificationLog} from "@/db/schema/11_notification-log"; +import {NotificationChannel} from "@/db/schema/09_notification-channel"; +import {Json} from "drizzle-zod"; + +export async function dispatchStorage( + payload: EventPayload, + policyId?: string, + channelId?: string, + organizationId?: string +): Promise { + try { + let channel: NotificationChannel | null = null; + + if (policyId) { + const policyDb = await db.query.alertPolicy.findFirst({ + where: eq(drizzleDb.schemas.alertPolicy.id, policyId), + with: { + notificationChannel: true + }, + }); + + if (!policyDb || !policyDb.notificationChannel) { + return { + success: false, + channelId: "", + provider: null, + error: "Policy or associated channel not found", + }; + } + + if (!policyDb.enabled || !policyDb.notificationChannel.enabled) { + return { + success: false, + channelId: policyDb.notificationChannel.id, + provider: policyDb.notificationChannel.provider as any, + error: "Policy or channel is disabled", + }; + } + + channel = { + ...policyDb.notificationChannel, + config: policyDb.notificationChannel.config as Json, + }; + } + + if (channelId) { + const fetchedChannel = await db.query.notificationChannel.findFirst({ + where: eq(drizzleDb.schemas.notificationChannel.id, channelId), + }); + + if (!fetchedChannel) { + return { + success: false, + channelId: channelId, + provider: null, + error: "Channel not found", + }; + } + + channel = { + ...fetchedChannel, + config: fetchedChannel.config as Json, + }; + } + + if (!channel) { + return { + success: false, + channelId: channelId || "", + provider: null, + error: "No valid channel to dispatch notification", + }; + } + + + if (!channel.enabled) { + return { + success: false, + channelId: channelId || "", + provider: null, + error: "Channel not active", + }; + } + + const result = await dispatchViaProvider( + channel.provider, + channel.config, + {...payload, timestamp: payload.timestamp || new Date()}, + channel.id + ); + + const [log] = await db + .insert(notificationLog) + .values({ + channelId: channel.id, + policyId: policyId || null, + organizationId: organizationId || null, + + provider: channel.provider, + providerName: channel.name, + event: payload.event as EventKind, + + title: payload.title, + message: payload.message, + level: payload.level, + payload: payload.data || null, + success: result.success, + error: result.success ? null : result.error, + providerResponse: result.response || null, + }) + .returning({id: notificationLog.id}); + + return {...result, channelId: channel.id}; + + } catch (err: any) { + return { + success: false, + channelId: channelId || "", + provider: null, + error: err?.message || "Unexpected error during dispatch", + }; + } +}