{defaultValues && (
-
+
)}
diff --git a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button.tsx
new file mode 100644
index 00000000..39b52903
--- /dev/null
+++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button.tsx
@@ -0,0 +1,76 @@
+"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, ShieldCheck} from "lucide-react";
+import {toast} from "sonner";
+import {useIsMobile} from "@/hooks/use-mobile";
+import {cn} from "@/lib/utils";
+import {StorageChannel} from "@/db/schema/12_storage-channel";
+import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
+
+type NotifierTestChannelButtonProps = {
+ channel: NotificationChannel | StorageChannel;
+ organizationId?: string;
+ kind: ChannelKind;
+}
+
+export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestChannelButtonProps) => {
+ const channelText = getChannelTextBasedOnKind(kind)
+
+ const isMobile = useIsMobile()
+ const mutation = useMutation({
+ mutationFn: async () => {
+ if (kind === "notification") {
+ const payload: EventPayload = {
+ title: 'Test Channel',
+ message: `We are testing channel ${channel.name}`,
+ level: 'info',
+ };
+ const result = await dispatchNotification(payload, undefined, channel.id, organizationId);
+
+ if (result.success) {
+ toast.success(result.message);
+ } else {
+ toast.error("An error occurred while testing the notification channel");
+ }
+ } else {
+ toast.error("Not yet supported");
+ }
+
+ },
+ });
+
+
+ return (
+
+ )
+}
\ 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",
+ };
+ }
+}