From 91f25bc25d782c57527f576ad5983a6c16b44f7e Mon Sep 17 00:00:00 2001 From: charlesgauthereau Date: Wed, 14 Jan 2026 21:54:13 +0100 Subject: [PATCH] feat: Working on storage modal in database page in order to add multiple backend storages. Adding a system to manage independently channels from organizations and from system admin. --- .../(admin)/notifications/channels/page.tsx | 6 +- .../(admin)/storages/channels/page.tsx | 4 +- .../database/[databaseId]/page.tsx | 29 +- .../(organization)/settings/page.tsx | 3 + .../channel/channel-add-edit-modal.tsx | 4 - .../channel/channel-card/channel-card.tsx | 34 +- .../channel/channel-form/channel-form.tsx | 5 +- .../providers/notifications/action.ts | 1 + .../channel-form/providers/storages/action.ts | 3 +- .../alert-policy/alert-policy.action.ts | 185 -- .../alert-policy/alert-policy.schema.ts | 27 - .../policy-form.tsx} | 365 ++- .../policy-modal.tsx} | 46 +- .../database/channels-policy/policy.action.ts | 351 +++ .../database/channels-policy/policy.schema.ts | 27 + .../organization-storages-tab.tsx | 60 + .../organization/tabs/organization-tabs.tsx | 22 +- src/db/index.ts | 4 +- src/db/migrations/0022_purple_retro_girl.sql | 16 + src/db/migrations/0023_common_the_captain.sql | 2 + src/db/migrations/0024_lush_blindfold.sql | 2 + src/db/migrations/meta/0022_snapshot.json | 2258 ++++++++++++++++ src/db/migrations/meta/0023_snapshot.json | 2278 ++++++++++++++++ src/db/migrations/meta/0024_snapshot.json | 2298 +++++++++++++++++ src/db/migrations/meta/_journal.json | 21 + src/db/schema/07_database.ts | 3 +- src/db/schema/09_notification-channel.ts | 1 + src/db/schema/12_storage-channel.ts | 2 + src/db/schema/14_storage-backup.ts | 38 + src/db/services/notification-channel.ts | 2 + src/db/services/storage-channel.ts | 25 + src/lib/acl/organization-acl.ts | 2 + 32 files changed, 7663 insertions(+), 461 deletions(-) delete mode 100644 src/components/wrappers/dashboard/database/alert-policy/alert-policy.action.ts delete mode 100644 src/components/wrappers/dashboard/database/alert-policy/alert-policy.schema.ts rename src/components/wrappers/dashboard/database/{alert-policy/alert-policy-form.tsx => channels-policy/policy-form.tsx} (53%) rename src/components/wrappers/dashboard/database/{alert-policy/alert-policy-modal.tsx => channels-policy/policy-modal.tsx} (51%) create mode 100644 src/components/wrappers/dashboard/database/channels-policy/policy.action.ts create mode 100644 src/components/wrappers/dashboard/database/channels-policy/policy.schema.ts create mode 100644 src/components/wrappers/dashboard/organization/tabs/organization-notifiers-tab/organization-storages-tab.tsx create mode 100644 src/db/migrations/0022_purple_retro_girl.sql create mode 100644 src/db/migrations/0023_common_the_captain.sql create mode 100644 src/db/migrations/0024_lush_blindfold.sql create mode 100644 src/db/migrations/meta/0022_snapshot.json create mode 100644 src/db/migrations/meta/0023_snapshot.json create mode 100644 src/db/migrations/meta/0024_snapshot.json create mode 100644 src/db/schema/14_storage-backup.ts create mode 100644 src/db/services/storage-channel.ts diff --git a/app/(customer)/dashboard/(admin)/notifications/channels/page.tsx b/app/(customer)/dashboard/(admin)/notifications/channels/page.tsx index 00f2e2e9..34001921 100644 --- a/app/(customer)/dashboard/(admin)/notifications/channels/page.tsx +++ b/app/(customer)/dashboard/(admin)/notifications/channels/page.tsx @@ -1,12 +1,8 @@ import {PageParams} from "@/types/next"; import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Metadata} from "next"; -import { - NotificationChannelsSection -} from "@/components/wrappers/dashboard/admin/notifications/channels/notification-channels-section"; 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 {notificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel"; 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"; diff --git a/app/(customer)/dashboard/(admin)/storages/channels/page.tsx b/app/(customer)/dashboard/(admin)/storages/channels/page.tsx index 898f1131..f3d0a5f8 100644 --- a/app/(customer)/dashboard/(admin)/storages/channels/page.tsx +++ b/app/(customer)/dashboard/(admin)/storages/channels/page.tsx @@ -1,10 +1,9 @@ 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 {desc, isNotNull, isNull, not} 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"; @@ -19,6 +18,7 @@ export default async function RoutePage(props: PageParams<{}>) { with: { organizations: true }, + where: isNull(drizzleDb.schemas.storageChannel.organizationId), orderBy: desc(drizzleDb.schemas.storageChannel.createdAt) }) as StorageChannelWith[] diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx index 52f8923e..b8d72a97 100644 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx @@ -12,9 +12,11 @@ import {getOrganizationProjectDatabases} from "@/lib/services"; import {getActiveMember, getOrganization} from "@/lib/auth/auth"; import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet"; import {capitalizeFirstLetter} from "@/utils/text"; -import {AlertPolicyModal} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy-modal"; import {getOrganizationChannels} from "@/db/services/notification-channel"; import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal"; +import {getOrganizationStorageChannels} from "@/db/services/storage-channel"; +import {ChannelPoliciesModal} from "@/components/wrappers/dashboard/database/channels-policy/policy-modal"; +import {HardDrive, Megaphone} from "lucide-react"; export default async function RoutePage(props: PageParams<{ projectId: string; @@ -39,7 +41,8 @@ export default async function RoutePage(props: PageParams<{ with: { project: true, retentionPolicy: true, - alertPolicies: true + alertPolicies: true, + storagePolicies: true } }); @@ -87,6 +90,10 @@ export default async function RoutePage(props: PageParams<{ const organizationChannels = await getOrganizationChannels(organization.id); const activeOrganizationChannels = organizationChannels.filter(channel => channel.enabled); + const organizationStorageChannels = await getOrganizationStorageChannels(organization.id); + const activeOrganizationStorageChannels = organizationStorageChannels.filter(channel => channel.enabled); + + const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null; const isMember = activeMember?.role === "member"; @@ -106,8 +113,22 @@ export default async function RoutePage(props: PageParams<{ {/**/} - + {/**/} + } + channels={activeOrganizationChannels} + organizationId={organization.id} + /> + } + kind={"storage"} + channels={activeOrganizationStorageChannels} + organizationId={organization.id} + />
diff --git a/app/(customer)/dashboard/(organization)/settings/page.tsx b/app/(customer)/dashboard/(organization)/settings/page.tsx index 6974a927..cfc6db26 100644 --- a/app/(customer)/dashboard/(organization)/settings/page.tsx +++ b/app/(customer)/dashboard/(organization)/settings/page.tsx @@ -11,6 +11,7 @@ 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 {getOrganizationStorageChannels} from "@/db/services/storage-channel"; export const metadata: Metadata = { title: "Settings", @@ -26,6 +27,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) { } const notificationChannels = await getOrganizationChannels(organization.id) + const storageChannels = await getOrganizationStorageChannels(organization.id) const permissions = computeOrganizationPermissions(activeMember); @@ -55,6 +57,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) { activeMember={activeMember} organization={organization} notificationChannels={notificationChannels} + storageChannels={storageChannels} /> diff --git a/src/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal.tsx index 8e63dcb8..3506e52a 100644 --- a/src/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal.tsx +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal.tsx @@ -74,7 +74,6 @@ export const ChannelAddEditModal = ({ } )} - e.preventDefault()}> {isCreate ? "Add" : "Edit"} {channelText} Channel @@ -82,11 +81,8 @@ export const ChannelAddEditModal = ({ Configure your {channelText.toLowerCase()} channel preferences. - -
{adminView ? - Configuration diff --git a/src/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card.tsx index 7529e6da..efcb71f4 100644 --- a/src/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card.tsx +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card.tsx @@ -25,9 +25,12 @@ export type ChannelCardProps = { export const ChannelCard = (props: ChannelCardProps) => { - const {data, organization, kind} = props; + const {data, organization, kind, adminView} = props; const isMobile = useIsMobile() + const isOwned = data.organizationId ? true : !organization; + const isLocalSystem = data.provider == "local"; + return (
@@ -38,7 +41,6 @@ export const ChannelCard = (props: ChannelCardProps) => {
-

{isMobile ? truncateWords(data.name, 2) : data.name}

@@ -49,18 +51,22 @@ export const ChannelCard = (props: ChannelCardProps) => {
{kind && (
- - + {(isOwned && !isLocalSystem) && ( + <> + + + + )}
)} diff --git a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.tsx index 456cc449..a8bdd60d 100644 --- a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.tsx +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.tsx @@ -94,16 +94,13 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind} }); const provider = form.watch("provider"); - - const channelTypes = kind == "notification" ? notificationTypes : storageTypes - const selectedProviderDetails = channelTypes.find(t => t.value === provider); if (isCreate && !provider) { return (
- {channelTypes.map((type) => { + {channelTypes.filter(p => p.value != "local").map((type) => { const Icon = type.icon; return ( > => { - try { - - const valuesToInsert = parsedInput.alertPolicies.map((policy) => ({ - databaseId: parsedInput.databaseId, - notificationChannelId: policy.notificationChannelId, - eventKinds: policy.eventKinds, - enabled: policy.enabled, - })); - - const insertedPolicies = await db - .insert(drizzleDb.schemas.alertPolicy) - .values(valuesToInsert) - .returning(); - - return { - success: true, - value: insertedPolicies, - actionSuccess: { - message: `Alert policies successfully added`, - }, - }; - - } catch (error) { - return { - success: false, - actionError: { - message: "Failed to add policies.", - status: 500, - cause: error instanceof Error ? error.message : "Unknown error", - }, - }; - } - }); - - - - -export const updateAlertPoliciesAction = userAction - .schema( - z.object({ - databaseId: z.string().min(1), - alertPolicies: z.array(AlertPolicySchema), - }) - ) - .action(async ({parsedInput}): Promise> => { - const {databaseId, alertPolicies} = parsedInput; - - try { - - const updatedPolicies = await db.transaction(async (tx) => { - const results: AlertPolicy[] = []; - for (const policy of alertPolicies) { - const {notificationChannelId, ...updateData} = policy; - - const updated = await tx - .update(drizzleDb.schemas.alertPolicy) - .set(withUpdatedAt({ - ...updateData, - })) - .where( - and( - eq(drizzleDb.schemas.alertPolicy.notificationChannelId, notificationChannelId), - eq(drizzleDb.schemas.alertPolicy.databaseId, databaseId) - ) - ) - .returning(); - - if (updated[0]) { - results.push(updated[0]); - } - } - - return results; - }); - - if (updatedPolicies.length === 0) { - return { - success: false, - actionError: {message: "No policies were updated."}, - }; - } - - return { - success: true, - value: updatedPolicies, - actionSuccess: { - message: `Successfully updated ${updatedPolicies.length} alert policy(ies).`, - }, - }; - } catch (error) { - console.error("Update alert policies failed:", error); - return { - success: false, - actionError: { - message: "Failed to update alert policies.", - status: 500, - cause: error instanceof Error ? error.message : "Unknown error", - }, - }; - } - }); - - -export const deleteAlertPoliciesAction = userAction - .schema( - z.object({ - databaseId: z.string().min(1), - alertPolicies: z.array(AlertPolicySchema), - }) - ) - .action(async ({ parsedInput }): Promise> => { - const { databaseId, alertPolicies } = parsedInput; - - try { - - const notificationChannelIds = alertPolicies.map((alertPolicy) => alertPolicy.notificationChannelId); - - const policiesToDelete = await db - .select() - .from(drizzleDb.schemas.alertPolicy) - .where( - and( - eq(drizzleDb.schemas.alertPolicy.databaseId, databaseId), - inArray(drizzleDb.schemas.alertPolicy.notificationChannelId, notificationChannelIds) - ) - ); - - if (policiesToDelete.length === 0) { - return { - success: false, - actionError: { message: "No alert policies found to delete." }, - }; - } - - await db - .delete(drizzleDb.schemas.alertPolicy) - .where( - and( - eq(drizzleDb.schemas.alertPolicy.databaseId, databaseId), - inArray(drizzleDb.schemas.alertPolicy.notificationChannelId, notificationChannelIds) - ) - ); - - return { - success: true, - value: policiesToDelete, - actionSuccess: { - message: `Successfully deleted ${policiesToDelete.length} alert policy(ies).`, - }, - }; - } catch (error) { - console.error("Delete alert policies failed:", error); - return { - success: false, - actionError: { - message: "Failed to delete alert policies.", - status: 500, - cause: error instanceof Error ? error.message : "Unknown error", - }, - }; - } - }); \ No newline at end of file diff --git a/src/components/wrappers/dashboard/database/alert-policy/alert-policy.schema.ts b/src/components/wrappers/dashboard/database/alert-policy/alert-policy.schema.ts deleted file mode 100644 index 73a4b265..00000000 --- a/src/components/wrappers/dashboard/database/alert-policy/alert-policy.schema.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {z} from "zod"; - - -export const AlertPolicySchema = - z.object({ - notificationChannelId: z.string().min(1, "Please select a notification channel"), - eventKinds: z.enum(['error_backup', 'error_restore', 'success_restore', 'success_backup', 'weekly_report']).array().nonempty(), - enabled: z.boolean().default(true), - } - ) - -export const AlertPoliciesSchema = z.object({ - alertPolicies: z.array(AlertPolicySchema) -}); - - -export type AlertPoliciesType = z.infer; -export type AlertPolicyType = z.infer; - - -export const EVENT_KIND_OPTIONS = [ - {label: "Error Backup", value: "error_backup"}, - {label: "Error Restore", value: "error_restore"}, - {label: "Success Restore", value: "success_restore"}, - {label: "Success Backup", value: "success_backup"}, - {label: "Weekly Report", value: "weekly_report"}, -]; \ No newline at end of file diff --git a/src/components/wrappers/dashboard/database/alert-policy/alert-policy-form.tsx b/src/components/wrappers/dashboard/database/channels-policy/policy-form.tsx similarity index 53% rename from src/components/wrappers/dashboard/database/alert-policy/alert-policy-form.tsx rename to src/components/wrappers/dashboard/database/channels-policy/policy-form.tsx index 8995ac2a..73940aa5 100644 --- a/src/components/wrappers/dashboard/database/alert-policy/alert-policy-form.tsx +++ b/src/components/wrappers/dashboard/database/channels-policy/policy-form.tsx @@ -1,12 +1,7 @@ import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form"; import {InfoIcon, Plus, Trash2} from "lucide-react"; import {useFieldArray} from "react-hook-form"; -import { - AlertPoliciesSchema, AlertPoliciesType, AlertPolicyType, - EVENT_KIND_OPTIONS -} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy.schema"; import {DatabaseWith} from "@/db/schema/07_database"; -import {AlertPolicy} from "@/db/schema/10_alert-policy"; import {NotificationChannel} from "@/db/schema/09_notification-channel"; import {Label} from "@/components/ui/label"; import {Button} from "@/components/ui/button"; @@ -15,192 +10,192 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/c import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select"; import {useMutation} from "@tanstack/react-query"; import {toast} from "sonner"; -import { - createAlertPoliciesAction, deleteAlertPoliciesAction, - updateAlertPoliciesAction -} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy.action"; import {useRouter} from "next/navigation"; import {Switch} from "@/components/ui/switch"; import {Card} from "@/components/ui/card"; import Link from "next/link"; import {useIsMobile} from "@/hooks/use-mobile"; -import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common"; +import { + ChannelKind, + getChannelIcon, + getChannelTextBasedOnKind +} from "@/components/wrappers/dashboard/admin/channels/helpers/common"; +import {StorageChannel} from "@/db/schema/12_storage-channel"; +import { + EVENT_KIND_OPTIONS, + PoliciesSchema, + PoliciesType, + PolicyType +} from "@/components/wrappers/dashboard/database/channels-policy/policy.schema"; +import { + createAlertPoliciesAction, createStoragePoliciesAction, deleteAlertPoliciesAction, deleteStoragePoliciesAction, + updateAlertPoliciesAction, updateStoragePoliciesAction +} from "@/components/wrappers/dashboard/database/channels-policy/policy.action"; -type AlertPolicyFormProps = { +type ChannelPoliciesFormProps = { onSuccess?: () => void; - notificationChannels: NotificationChannel[]; + channels: NotificationChannel[] | StorageChannel[]; organizationId: string; database: DatabaseWith; + kind: ChannelKind }; -export const AlertPolicyForm = ({database, notificationChannels, organizationId, onSuccess}: AlertPolicyFormProps) => { - const router = useRouter() - const isMobile = useIsMobile() - const organizationNotificationChannels = notificationChannels.map(channel => channel.id) ?? []; +export const ChannelPoliciesForm = ({ + database, + channels, + organizationId, + onSuccess, + kind + }: ChannelPoliciesFormProps) => { + const router = useRouter(); + const isMobile = useIsMobile(); + const channelText = getChannelTextBasedOnKind(kind); - const formattedAlertPoliciesList = (alertPolicies: AlertPolicy[]) => { - return alertPolicies.filter((alertPolicy) => organizationNotificationChannels.includes(alertPolicy.notificationChannelId)).map((alertPolicy) => ({ - notificationChannelId: alertPolicy.notificationChannelId, - eventKinds: alertPolicy.eventKinds, - enabled: alertPolicy.enabled + const organizationChannels = channels.map(c => c.id); + + const filterByChannel = ( + items: T[] | undefined | null, + channelKey: K + ): T[] => items?.filter(item => organizationChannels.includes(item[channelKey] as string)) ?? []; + + const formattedAlertPolicies = filterByChannel(database.alertPolicies, "notificationChannelId") + .map(({notificationChannelId, eventKinds, enabled}) => ({ + channelId: notificationChannelId, + eventKinds, + enabled })); - }; + + const formattedStoragePolicies = filterByChannel(database.storagePolicies, "storageChannelId") + .map(({storageChannelId, enabled}) => ({ + channelId: storageChannelId, + enabled + })); + + const defaultPolicies: PolicyType[] = + kind === "notification" + ? formattedAlertPolicies + : formattedStoragePolicies.map(({ channelId, enabled }) => ({ channelId, enabled })); const form = useZodForm({ - schema: AlertPoliciesSchema, - defaultValues: { - alertPolicies: database.alertPolicies && database.alertPolicies.length > 0 ? formattedAlertPoliciesList(database.alertPolicies) : [], - }, + schema: PoliciesSchema, + defaultValues: { policies: defaultPolicies }, + context: { kind } }); - const {fields, append, remove} = useFieldArray({ - control: form.control, - name: "alertPolicies", - }) - - - const addAlertPolicy = () => { - append({notificationChannelId: "", eventKinds: [], enabled: true}); - } - - const removeAlertPolicy = (index: number) => { - remove(index) - } - - const onCancel = () => { - form.reset() - onSuccess?.() - } + const {fields, append, remove} = useFieldArray({ control: form.control, name: "policies" }); + const addPolicy = () => append({channelId: "", eventKinds: [], enabled: true}); + const removePolicyHandler = (index: number) => remove(index); + const onCancel = () => { form.reset(); onSuccess?.(); }; const mutation = useMutation({ - mutationFn: async ({alertPolicies}: AlertPoliciesType) => { - const defaultFormatedAlertPolicies = formattedAlertPoliciesList(database?.alertPolicies ?? []); + mutationFn: async ({policies}: PoliciesType) => { + const payload = policies.map(p => kind === "notification" ? p : { ...p, eventKinds: undefined }); - const alertPoliciesToAdd = alertPolicies?.filter( - (alertPolicy) => !defaultFormatedAlertPolicies.some((a) => a.notificationChannelId == alertPolicy.notificationChannelId) - ) ?? []; - - const alertPoliciesToRemove = defaultFormatedAlertPolicies.filter( - (alertPolicy) => !alertPolicies?.some((v) => v.notificationChannelId === alertPolicy.notificationChannelId) - ) ?? []; - - const alertPoliciesToUpdate = alertPolicies?.filter((alertPolicy) => { - const existing = defaultFormatedAlertPolicies.find((a) => a.notificationChannelId === alertPolicy.notificationChannelId); + const policiesToAdd = payload.filter( + (policy) => !defaultPolicies.some((a) => a.channelId === policy.channelId) + ); + const policiesToRemove = defaultPolicies.filter( + (policy) => !payload.some((v) => v.channelId === policy.channelId) + ); + const policiesToUpdate = payload.filter((policy) => { + const existing = defaultPolicies.find((a) => a.channelId === policy.channelId); return existing && - (existing.eventKinds !== alertPolicy.eventKinds || existing.enabled !== alertPolicy.enabled); - }) ?? []; + (existing.eventKinds !== policy.eventKinds || existing.enabled !== policy.enabled); + }); - const results = await Promise.allSettled([ - alertPoliciesToAdd.length > 0 - ? createAlertPoliciesAction({ - databaseId: database.id, - alertPolicies: alertPoliciesToAdd, - }) - : Promise.resolve(null), + console.log(policiesToUpdate); + console.log(policiesToAdd); - alertPoliciesToUpdate.length > 0 - ? updateAlertPoliciesAction({ - databaseId: database.id, - alertPolicies: alertPoliciesToUpdate, - }) - : Promise.resolve(null), - - alertPoliciesToRemove.length > 0 - ? deleteAlertPoliciesAction({ - databaseId: database.id, - alertPolicies: alertPoliciesToRemove as AlertPolicyType[], - }) - : Promise.resolve(null), - ]); + const promises = kind === "notification" + ? [ + policiesToAdd.length > 0 ? await createAlertPoliciesAction({databaseId: database.id, alertPolicies: policiesToAdd}) : null, + policiesToUpdate.length > 0 ? await updateAlertPoliciesAction({databaseId: database.id, alertPolicies: policiesToUpdate}) : null, + policiesToRemove.length > 0 ? await deleteAlertPoliciesAction({databaseId: database.id, alertPolicies: policiesToRemove}) : null, + ] + : [ + policiesToAdd.length > 0 ? await createStoragePoliciesAction({databaseId: database.id, storagePolicies: policiesToAdd}) : null, + policiesToUpdate.length > 0 ? await updateStoragePoliciesAction({databaseId: database.id, storagePolicies: policiesToUpdate}) : null, + policiesToRemove.length > 0 ? await deleteStoragePoliciesAction({databaseId: database.id, storagePolicies: policiesToRemove}) : null, + ]; + const results = await Promise.allSettled(promises); const rejected = results.find((r): r is PromiseRejectedResult => r.status === "rejected"); - if (rejected) { - throw new Error(rejected.reason?.message || "Network or server error"); - } + if (rejected) throw new Error(rejected.reason?.message || "Network or server error"); const failedActions = results .filter((r): r is PromiseFulfilledResult => r.status === "fulfilled") .map(r => r.value) - .filter((value): value is { data: { success: false; actionError: any } } => - value !== null && typeof value === "object" && value.data.success === false - ); - - - if (failedActions.length > 0) { - const firstError = failedActions[0].data.actionError; - const message = firstError?.message || "One or more operations failed"; - throw new Error(message); - } + .filter((v): v is { data: { success: false; actionError: any } } => v !== null && v.data?.success === false); + if (failedActions.length > 0) throw new Error(failedActions[0].data.actionError?.message || "One or more operations failed"); return {success: true}; }, - onSuccess: () => { - toast.success("Alert policies saved successfully"); - //onSuccess?.(); - router.refresh(); - }, - onError: (error: any) => { - toast.error(error.message || "Failed to save alert policies"); - }, + onSuccess: () => { toast.success("Policies saved successfully"); router.refresh(); }, + onError: (error: any) => { toast.error(error.message || "Failed to save policies"); }, }); - return ( -
{ - await mutation.mutateAsync(values); - }} - > + { + + if (kind === "notification") { + for (const policy of values.policies) { + if (!policy.eventKinds || policy.eventKinds.length === 0) { + toast.error("Please select at least one event for all notification policies"); + return; + } + } + } + + await mutation.mutateAsync(values) + } + }>
- + {!isMobile && (

- Choose which channels receive notifications for specific events. + {kind === "notification" + ? "Choose which channels receive notifications for specific events." + : "Choose which storage to use with your database"}

)}
- {organizationNotificationChannels.length === 0 ? ( -
+ {channels.length === 0 ? ( +
-

No notification channels

+

No channels

- Please configure - notification channels in your organization settings first. + Please + configure {channelText.toLowerCase()} channels + in your organization settings first.

) : fields.length === 0 ? ( -
+
-

No alert policies

+

No policies

- Click "Add Policy" to start receiving notifications. + {kind === "notification" ? `Click "Add Policy" to start receiving notifications.` : `Click "Add Policy" to use this storage.`}

) : ( @@ -211,24 +206,17 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
{ - const selectedIds = form - .watch("alertPolicies") - .map((a: AlertPolicyType) => a.notificationChannelId) - .filter(Boolean); - - const availableNotificationChannels = notificationChannels.filter( - (channel) => - channel.id.toString() === field.value?.toString() || - !selectedIds.includes(channel.id.toString()) + name={`policies.${index}.channelId`} + render={({field}) => { + const selectedIds = form.watch("policies").map((a: PolicyType) => a.channelId).filter(Boolean); + const availableChannels = channels.filter( + (channel) => channel.id.toString() === field.value?.toString() || !selectedIds.includes(channel.id.toString()) ); - - const selectedChannel = notificationChannels.find(c => c.id === field.value); + const selectedChannel = channels.find(c => c.id === field.value); return ( @@ -242,10 +230,10 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId, {getChannelIcon(selectedChannel.provider)}
- {selectedChannel.name} + {selectedChannel.name} - {selectedChannel.provider} + {selectedChannel.provider}
)} @@ -253,16 +241,12 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId, - {availableNotificationChannels.map(channel => ( + {availableChannels.map(channel => (
-
- {getChannelIcon(channel.provider)} -
+
{getChannelIcon(channel.provider)}
{channel.name} - - ({channel.provider}) - + ({channel.provider})
))} @@ -274,14 +258,13 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId, }} />
+
- + ( + name={`policies.${index}.enabled`} + render={({field}) => (
@@ -297,43 +280,41 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId, )} />
+
-
- ( - - - Trigger Events - - -
- -
-
- -
- )} - /> + + {kind === "notification" && ( + ( + + Trigger Events + +
+ +
+
+ +
+ )} + /> + )}
))} @@ -343,12 +324,8 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
- - Cancel - - - Save Changes - + Cancel + Save Changes
); diff --git a/src/components/wrappers/dashboard/database/alert-policy/alert-policy-modal.tsx b/src/components/wrappers/dashboard/database/channels-policy/policy-modal.tsx similarity index 51% rename from src/components/wrappers/dashboard/database/alert-policy/alert-policy-modal.tsx rename to src/components/wrappers/dashboard/database/channels-policy/policy-modal.tsx index 9279fee7..eb9ad6d9 100644 --- a/src/components/wrappers/dashboard/database/alert-policy/alert-policy-modal.tsx +++ b/src/components/wrappers/dashboard/database/channels-policy/policy-modal.tsx @@ -1,7 +1,5 @@ "use client" -import {Megaphone} from "lucide-react"; - -import {useState} from "react"; +import {ReactNode, useState} from "react"; import { Dialog, DialogContent, @@ -11,36 +9,50 @@ import { DialogTrigger } from "@/components/ui/dialog"; import {Button} from "@/components/ui/button"; -import {AlertPolicyForm} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy-form"; import {DatabaseWith} from "@/db/schema/07_database"; import {NotificationChannel} from "@/db/schema/09_notification-channel"; import {Separator} from "@/components/ui/separator"; import {Badge} from "@/components/ui/badge"; +import {StorageChannel} from "@/db/schema/12_storage-channel"; +import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common"; +import {ChannelPoliciesForm} from "@/components/wrappers/dashboard/database/channels-policy/policy-form"; -type AlertPolicyModalProps = { + +type ChannelPoliciesModalProps = { database: DatabaseWith; - notificationChannels: NotificationChannel[]; + channels: NotificationChannel[] | StorageChannel[]; organizationId: string; + kind: ChannelKind; + icon: ReactNode; + } -export const AlertPolicyModal = ({database, notificationChannels, organizationId}: AlertPolicyModalProps) => { +export const ChannelPoliciesModal = ({icon, kind, database, channels, organizationId}: ChannelPoliciesModalProps) => { const [open, setOpen] = useState(false); + const channelText = getChannelTextBasedOnKind(kind) - - const notificationsChannelsFiltered = notificationChannels + const channelsFiltered = channels .filter((channel) => channel.enabled) - const notificationsChannelsIds = notificationsChannelsFiltered + const channelsIds = channelsFiltered .map(channel => channel.id); + console.log(channelsIds); + const activeAlertPolicies = database.alertPolicies?.filter((policy) => channelsIds.includes(policy.notificationChannelId)); + const activeStoragePolicies = database.storagePolicies?.filter((policy) => channelsIds.includes(policy.storageChannelId)); - const activePolicies = database.alertPolicies?.filter((policy) => notificationsChannelsIds.includes(policy.notificationChannelId)); + console.log(channels); + console.log(database.storagePolicies); + console.log("activeAlertPolicies", activeAlertPolicies); + console.log("activeStoragePolicies", activeStoragePolicies); + + const activePolicies = kind === "notification" ? activeAlertPolicies : activeStoragePolicies; return (