mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
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.
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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[]
|
||||
|
||||
|
||||
+25
-4
@@ -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<{
|
||||
{/*<EditButton/>*/}
|
||||
<RetentionPolicySheet database={dbItem}/>
|
||||
<CronButton database={dbItem}/>
|
||||
<AlertPolicyModal database={dbItem} notificationChannels={activeOrganizationChannels}
|
||||
organizationId={organization.id}/>
|
||||
{/*<AlertPolicyModal database={dbItem} notificationChannels={activeOrganizationChannels}*/}
|
||||
{/* organizationId={organization.id}/>*/}
|
||||
<ChannelPoliciesModal
|
||||
database={dbItem}
|
||||
kind={"notification"}
|
||||
icon={<Megaphone/>}
|
||||
channels={activeOrganizationChannels}
|
||||
organizationId={organization.id}
|
||||
/>
|
||||
<ChannelPoliciesModal
|
||||
database={dbItem}
|
||||
icon={<HardDrive/>}
|
||||
kind={"storage"}
|
||||
channels={activeOrganizationStorageChannels}
|
||||
organizationId={organization.id}
|
||||
/>
|
||||
<ImportModal database={dbItem}/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
|
||||
@@ -74,7 +74,6 @@ export const ChannelAddEditModal = ({
|
||||
}
|
||||
</DialogTrigger>
|
||||
)}
|
||||
|
||||
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle> {isCreate ? "Add" : "Edit"} {channelText} Channel</DialogTitle>
|
||||
@@ -82,11 +81,8 @@ export const ChannelAddEditModal = ({
|
||||
Configure your {channelText.toLowerCase()} channel preferences.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<div>
|
||||
{adminView ?
|
||||
|
||||
<Tabs className="flex flex-col flex-1" defaultValue="configuration">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="configuration">Configuration</TabsTrigger>
|
||||
|
||||
+20
-14
@@ -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 (
|
||||
<div className="block transition-all duration-200 rounded-xl">
|
||||
<Card className="flex flex-row justify-between p-4">
|
||||
@@ -38,7 +41,6 @@ export const ChannelCard = (props: ChannelCardProps) => {
|
||||
</div>
|
||||
<div className={`h-2 w-2 rounded-full ${data.enabled ? "bg-green-600" : "bg-muted"}`}/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-start w-full">
|
||||
<div className="flex flex-col items-start md:flex-row md:items-center gap-2 ">
|
||||
<h3 className="font-medium text-foreground">{isMobile ? truncateWords(data.name, 2) : data.name}</h3>
|
||||
@@ -49,18 +51,22 @@ export const ChannelCard = (props: ChannelCardProps) => {
|
||||
</div>
|
||||
{kind && (
|
||||
<div className="flex items-center gap-2">
|
||||
<EditChannelButton
|
||||
organizations={props.organizations}
|
||||
adminView={props.adminView}
|
||||
organization={organization}
|
||||
channel={data}
|
||||
kind={kind}
|
||||
/>
|
||||
<DeleteChannelButton
|
||||
kind={kind}
|
||||
organizationId={organization?.id}
|
||||
channelId={data.id}
|
||||
/>
|
||||
{(isOwned && !isLocalSystem) && (
|
||||
<>
|
||||
<EditChannelButton
|
||||
organizations={props.organizations}
|
||||
adminView={props.adminView}
|
||||
organization={organization}
|
||||
channel={data}
|
||||
kind={kind}
|
||||
/>
|
||||
<DeleteChannelButton
|
||||
kind={kind}
|
||||
organizationId={organization?.id}
|
||||
channelId={data.id}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
+1
-4
@@ -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 (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 py-4">
|
||||
{channelTypes.map((type) => {
|
||||
{channelTypes.filter(p => p.value != "local").map((type) => {
|
||||
const Icon = type.icon;
|
||||
return (
|
||||
<Card
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ export const addNotificationChannelAction = userAction.schema(
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
enabled: data.enabled ?? true,
|
||||
organizationId: organizationId ?? null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ export const addStorageChannelAction = userAction.schema(
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
enabled: data.enabled ?? true,
|
||||
organizationId: organizationId ?? null
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -75,7 +76,7 @@ export const removeStorageChannelAction = userAction.schema(
|
||||
try {
|
||||
if (organizationId) {
|
||||
await db
|
||||
.delete(drizzleDb.schemas.organizationNotificationChannel)
|
||||
.delete(drizzleDb.schemas.organizationStorageChannel)
|
||||
.where(
|
||||
and(
|
||||
eq(drizzleDb.schemas.organizationStorageChannel.organizationId, organizationId),
|
||||
|
||||
@@ -1,185 +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 * as drizzleDb from "@/db";
|
||||
import {and, eq, inArray} from "drizzle-orm";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {
|
||||
AlertPolicySchema
|
||||
} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy.schema";
|
||||
import {AlertPolicy} from "@/db/schema/10_alert-policy";
|
||||
|
||||
|
||||
|
||||
export const createAlertPoliciesAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
databaseId: z.string(),
|
||||
alertPolicies: z.array(AlertPolicySchema),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<AlertPolicy[]>> => {
|
||||
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<ServerActionResult<AlertPolicy[]>> => {
|
||||
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<ServerActionResult<AlertPolicy[]>> => {
|
||||
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",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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<typeof AlertPoliciesSchema>;
|
||||
export type AlertPolicyType = z.infer<typeof AlertPolicySchema>;
|
||||
|
||||
|
||||
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"},
|
||||
];
|
||||
+171
-194
@@ -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 = <T, K extends keyof T>(
|
||||
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<any> => 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 (
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-6"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<Form form={form} className="flex flex-col gap-6" onSubmit={
|
||||
async (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)
|
||||
}
|
||||
}>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-base font-medium">Configure Alerts</Label>
|
||||
<Label className="text-base font-medium">
|
||||
Configure {kind === "notification" ? "Alerts" : "Storages"}
|
||||
</Label>
|
||||
{!isMobile && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
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"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
disabled={
|
||||
fields.length >= notificationChannels.length ||
|
||||
notificationChannels.length === 0
|
||||
}
|
||||
disabled={fields.length >= channels.length || channels.length === 0}
|
||||
type="button"
|
||||
size="sm"
|
||||
className="h-8"
|
||||
onClick={addAlertPolicy}>
|
||||
<Plus className="w-4 h-4 mr-1.5"/>
|
||||
Add Policy
|
||||
onClick={addPolicy}>
|
||||
<Plus className="w-4 h-4 mr-1.5"/> Add Policy
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 w-full">
|
||||
{organizationNotificationChannels.length === 0 ? (
|
||||
<div
|
||||
className="flex flex-col items-center justify-center p-8 border border-dashed rounded-xl bg-muted/20 text-center gap-2">
|
||||
{channels.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center p-8 border border-dashed rounded-xl bg-muted/20 text-center gap-2">
|
||||
<InfoIcon className="h-8 w-8 text-muted-foreground/50"/>
|
||||
<p className="font-medium text-sm text-foreground">No notification channels</p>
|
||||
<p className="font-medium text-sm text-foreground">No channels</p>
|
||||
<p className="text-xs text-muted-foreground max-w-xs">
|
||||
Please <Link href={`/dashboard/settings`}
|
||||
className="underline underline-offset-4 hover:text-primary transition-colors">configure
|
||||
notification channels</Link> in your organization settings first.
|
||||
Please <Link href={`/dashboard/settings`} className="underline underline-offset-4 hover:text-primary transition-colors">
|
||||
configure {channelText.toLowerCase()} channels
|
||||
</Link> in your organization settings first.
|
||||
</p>
|
||||
</div>
|
||||
) : fields.length === 0 ? (
|
||||
<div
|
||||
className="flex flex-col items-center justify-center p-8 border border-dashed rounded-xl bg-muted/20 text-center gap-2">
|
||||
<div className="flex flex-col items-center justify-center p-8 border border-dashed rounded-xl bg-muted/20 text-center gap-2">
|
||||
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Plus className="h-4 w-4 text-primary"/>
|
||||
</div>
|
||||
<p className="font-medium text-sm text-foreground">No alert policies</p>
|
||||
<p className="font-medium text-sm text-foreground">No policies</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Click "Add Policy" to start receiving notifications.
|
||||
{kind === "notification" ? `Click "Add Policy" to start receiving notifications.` : `Click "Add Policy" to use this storage.`}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -211,24 +206,17 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
<div className="flex flex-row gap-2 items-start md:items-end flex-nowrap min-w-0 ">
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-1.5">
|
||||
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">
|
||||
Notification Channel
|
||||
{channelText} Channel
|
||||
</Label>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`alertPolicies.${index}.notificationChannelId`}
|
||||
render={({ field }) => {
|
||||
const selectedIds = form
|
||||
.watch("alertPolicies")
|
||||
.map((a: AlertPolicyType) => a.notificationChannelId)
|
||||
.filter(Boolean);
|
||||
|
||||
const availableNotificationChannels = notificationChannels.filter(
|
||||
(channel) =>
|
||||
channel.id.toString() === field.value?.toString() ||
|
||||
!selectedIds.includes(channel.id.toString())
|
||||
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 (
|
||||
<FormItem className="space-y-0 min-w-0">
|
||||
@@ -242,10 +230,10 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
{getChannelIcon(selectedChannel.provider)}
|
||||
</div>
|
||||
<span className="truncate font-medium text-sm min-w-0">
|
||||
{selectedChannel.name}
|
||||
{selectedChannel.name}
|
||||
</span>
|
||||
<span className="shrink-0 text-[9px] bg-secondary px-1.5 py-0.5 rounded text-muted-foreground font-mono uppercase">
|
||||
{selectedChannel.provider}
|
||||
{selectedChannel.provider}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -253,16 +241,12 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{availableNotificationChannels.map(channel => (
|
||||
{availableChannels.map(channel => (
|
||||
<SelectItem key={channel.id.toString()} value={channel.id.toString()}>
|
||||
<div className="flex items-center gap-2 w-full min-w-0">
|
||||
<div className="text-muted-foreground scale-90 shrink-0">
|
||||
{getChannelIcon(channel.provider)}
|
||||
</div>
|
||||
<div className="text-muted-foreground scale-90 shrink-0">{getChannelIcon(channel.provider)}</div>
|
||||
<span className="font-medium truncate min-w-0">{channel.name}</span>
|
||||
<span className="text-xs text-muted-foreground ml-2 capitalize shrink-0">
|
||||
({channel.provider})
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground ml-2 capitalize shrink-0">({channel.provider})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -274,14 +258,13 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5 shrink-0">
|
||||
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">
|
||||
Status
|
||||
</Label>
|
||||
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">Status</Label>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`alertPolicies.${index}.enabled`}
|
||||
render={({ field }) => (
|
||||
name={`policies.${index}.enabled`}
|
||||
render={({field}) => (
|
||||
<FormItem className="space-y-0">
|
||||
<FormControl>
|
||||
<div className="flex items-center h-9 px-1 md:px-3 rounded-md border border-input bg-background justify-between min-w-0">
|
||||
@@ -297,43 +280,41 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5 shrink-0 mt-auto">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 text-muted-foreground hover:text-destructive hover:border-destructive/50 hover:bg-destructive/10 transition-colors border-input bg-background"
|
||||
onClick={() => removeAlertPolicy(index)}
|
||||
>
|
||||
<Button type="button" variant="outline" size="icon"
|
||||
className="h-9 w-9 text-muted-foreground hover:text-destructive hover:border-destructive/50 hover:bg-destructive/10 transition-colors border-input bg-background"
|
||||
onClick={() => removePolicyHandler(index)}>
|
||||
<Trash2 className="w-4 h-4"/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`alertPolicies.${index}.eventKinds`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-1.5 min-w-0">
|
||||
<FormLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
Trigger Events
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="max-w-full overflow-hidden">
|
||||
<MultiSelect
|
||||
options={EVENT_KIND_OPTIONS}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ?? []}
|
||||
placeholder={isMobile ? "Select events...": "Select events to trigger notifications..."}
|
||||
variant="inverted"
|
||||
animation={0}
|
||||
className="bg-background/50 w-full min-w-0 flex-wrap"
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{kind === "notification" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`policies.${index}.eventKinds`}
|
||||
render={({field}) => (
|
||||
<FormItem className="space-y-1.5 min-w-0">
|
||||
<FormLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Trigger Events</FormLabel>
|
||||
<FormControl>
|
||||
<div className="max-w-full overflow-hidden">
|
||||
<MultiSelect
|
||||
options={EVENT_KIND_OPTIONS}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ?? []}
|
||||
placeholder={isMobile ? "Select events..." : "Select events to trigger notifications..."}
|
||||
variant="inverted"
|
||||
animation={0}
|
||||
className="bg-background/50 w-full min-w-0 flex-wrap"
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
@@ -343,12 +324,8 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-end pt-2 border-t mt-2">
|
||||
<ButtonWithLoading variant="outline" type="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</ButtonWithLoading>
|
||||
<ButtonWithLoading isPending={mutation.isPending}>
|
||||
Save Changes
|
||||
</ButtonWithLoading>
|
||||
<ButtonWithLoading variant="outline" type="button" onClick={onCancel}>Cancel</ButtonWithLoading>
|
||||
<ButtonWithLoading isPending={mutation.isPending}>Save Changes</ButtonWithLoading>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
+29
-17
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" onClick={() => setOpen(true)} className="relative">
|
||||
<Megaphone/>
|
||||
{icon}
|
||||
{activePolicies && activePolicies.length > 0 && (
|
||||
<Badge
|
||||
className="absolute -top-1.5 -right-1.5 h-4 w-4 rounded-full p-0 text-[10px] flex items-center justify-center"
|
||||
@@ -52,17 +64,17 @@ export const AlertPolicyModal = ({database, notificationChannels, organizationId
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Alert policies</DialogTitle>
|
||||
<DialogTitle>{channelText} policies</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add and manage your database alert policies
|
||||
Add and manage your database {channelText.toLowerCase()} policies
|
||||
</DialogDescription>
|
||||
|
||||
<Separator className="mt-3 mb-3"/>
|
||||
<AlertPolicyForm
|
||||
<ChannelPoliciesForm
|
||||
organizationId={organizationId}
|
||||
notificationChannels={notificationsChannelsFiltered}
|
||||
channels={channels}
|
||||
database={database}
|
||||
onSuccess={() => setOpen(false)}
|
||||
kind={kind}
|
||||
/>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
@@ -0,0 +1,351 @@
|
||||
"use server"
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {and, eq, inArray} from "drizzle-orm";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {AlertPolicy} from "@/db/schema/10_alert-policy";
|
||||
import {PolicySchema} from "@/components/wrappers/dashboard/database/channels-policy/policy.schema";
|
||||
import {StoragePolicy} from "@/db/schema/13_storage-policy";
|
||||
|
||||
|
||||
export const createAlertPoliciesAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
databaseId: z.string(),
|
||||
alertPolicies: z.array(PolicySchema),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<AlertPolicy[]>> => {
|
||||
try {
|
||||
|
||||
const valuesToInsert = parsedInput.alertPolicies.map((policy) => ({
|
||||
databaseId: parsedInput.databaseId,
|
||||
notificationChannelId: policy.channelId,
|
||||
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(PolicySchema),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<AlertPolicy[]>> => {
|
||||
const {databaseId, alertPolicies} = parsedInput;
|
||||
|
||||
try {
|
||||
|
||||
const updatedPolicies = await db.transaction(async (tx) => {
|
||||
const results: AlertPolicy[] = [];
|
||||
for (const policy of alertPolicies) {
|
||||
const {channelId: 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(PolicySchema),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<AlertPolicy[]>> => {
|
||||
const {databaseId, alertPolicies} = parsedInput;
|
||||
|
||||
try {
|
||||
|
||||
const notificationChannelIds = alertPolicies.map((alertPolicy) => alertPolicy.channelId);
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const createStoragePoliciesAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
databaseId: z.string(),
|
||||
storagePolicies: z.array(PolicySchema),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<StoragePolicy[]>> => {
|
||||
try {
|
||||
|
||||
const valuesToInsert = parsedInput.storagePolicies.map((policy) => ({
|
||||
databaseId: parsedInput.databaseId,
|
||||
storageChannelId: policy.channelId,
|
||||
enabled: policy.enabled,
|
||||
}));
|
||||
|
||||
|
||||
const insertedPolicies = await db
|
||||
.insert(drizzleDb.schemas.storagePolicy)
|
||||
.values(valuesToInsert)
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: insertedPolicies,
|
||||
actionSuccess: {
|
||||
message: `Storage 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 updateStoragePoliciesAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
databaseId: z.string().min(1),
|
||||
storagePolicies: z.array(PolicySchema),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<StoragePolicy[]>> => {
|
||||
const {databaseId, storagePolicies} = parsedInput;
|
||||
|
||||
try {
|
||||
|
||||
const updatedPolicies = await db.transaction(async (tx) => {
|
||||
const results: StoragePolicy[] = [];
|
||||
for (const policy of storagePolicies) {
|
||||
const {channelId: storageChannelId, ...updateData} = policy;
|
||||
|
||||
const updated = await tx
|
||||
.update(drizzleDb.schemas.storagePolicy)
|
||||
.set(withUpdatedAt({
|
||||
...updateData,
|
||||
}))
|
||||
.where(
|
||||
and(
|
||||
eq(drizzleDb.schemas.storagePolicy.storageChannelId, storageChannelId),
|
||||
eq(drizzleDb.schemas.storagePolicy.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} storage policy(ies).`,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Update storage policies failed:", error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update storage policies.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const deleteStoragePoliciesAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
databaseId: z.string().min(1),
|
||||
storagePolicies: z.array(PolicySchema),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<StoragePolicy[]>> => {
|
||||
const {databaseId, storagePolicies} = parsedInput;
|
||||
|
||||
try {
|
||||
|
||||
const storageChannelIds = storagePolicies.map((storagePolicy) => storagePolicy.channelId);
|
||||
|
||||
const policiesToDelete = await db
|
||||
.select()
|
||||
.from(drizzleDb.schemas.storagePolicy)
|
||||
.where(
|
||||
and(
|
||||
eq(drizzleDb.schemas.storagePolicy.databaseId, databaseId),
|
||||
inArray(drizzleDb.schemas.storagePolicy.storageChannelId, storageChannelIds)
|
||||
)
|
||||
);
|
||||
|
||||
if (policiesToDelete.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {message: "No storage policies found to delete."},
|
||||
};
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(drizzleDb.schemas.storagePolicy)
|
||||
.where(
|
||||
and(
|
||||
eq(drizzleDb.schemas.storagePolicy.databaseId, databaseId),
|
||||
inArray(drizzleDb.schemas.storagePolicy.storageChannelId, storageChannelIds)
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: policiesToDelete,
|
||||
actionSuccess: {
|
||||
message: `Successfully deleted ${policiesToDelete.length} storage policy(ies).`,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Delete storage policies failed:", error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to delete storage policies.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const PolicySchema = z.object({
|
||||
channelId: z.string().min(1, "Please select channel"),
|
||||
eventKinds: z.array(z.enum([
|
||||
'error_backup', 'error_restore', 'success_restore', 'success_backup', 'weekly_report'
|
||||
]))
|
||||
.optional(),
|
||||
enabled: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const PoliciesSchema = z.object({
|
||||
policies: z.array(PolicySchema)
|
||||
});
|
||||
|
||||
|
||||
export type PoliciesType = z.infer<typeof PoliciesSchema>;
|
||||
export type PolicyType = z.infer<typeof PolicySchema>;
|
||||
|
||||
|
||||
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"},
|
||||
];
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||
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";
|
||||
import {StorageChannel} from "@/db/schema/12_storage-channel";
|
||||
|
||||
export type OrganizationNotifiersTabProps = {
|
||||
organization: OrganizationWithMembers;
|
||||
storageChannels: StorageChannel[];
|
||||
};
|
||||
|
||||
export const OrganizationStoragesTab = ({
|
||||
organization,
|
||||
storageChannels,
|
||||
}: OrganizationNotifiersTabProps) => {
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
|
||||
const hasNotifiers = storageChannels.length > 0;
|
||||
const kind = "storage"
|
||||
return (
|
||||
<div className="flex flex-col gap-y-6 h-full py-4">
|
||||
<div className="h-full flex flex-col gap-y-6">
|
||||
<div className={cn("hidden flex-row justify-between items-start", hasNotifiers && "flex")}>
|
||||
<div className="max-w-2xl ">
|
||||
<h3 className="text-xl font-semibold text-balance mb-1">
|
||||
Notification Settings
|
||||
</h3>
|
||||
</div>
|
||||
<ChannelAddEditModal
|
||||
kind={kind}
|
||||
organization={organization}
|
||||
open={isAddModalOpen}
|
||||
onOpenChangeAction={setIsAddModalOpen}
|
||||
/>
|
||||
</div>
|
||||
{hasNotifiers ? (
|
||||
<div className="h-full">
|
||||
<CardsWithPagination
|
||||
data={storageChannels}
|
||||
cardItem={ChannelCard}
|
||||
cardsPerPage={8}
|
||||
numberOfColumns={2}
|
||||
organization={organization}
|
||||
kind={kind}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyStatePlaceholder
|
||||
text="No storage channels configured yet"
|
||||
onClick={() => setIsAddModalOpen(true)}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,14 +12,19 @@ import {
|
||||
} from "@/components/wrappers/dashboard/organization/tabs/organization-notifiers-tab/organization-notifiers-tab";
|
||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {useOrganizationPermissions} from "@/hooks/use-organization-permissions";
|
||||
import {
|
||||
OrganizationStoragesTab
|
||||
} from "@/components/wrappers/dashboard/organization/tabs/organization-notifiers-tab/organization-storages-tab";
|
||||
import {StorageChannel} from "@/db/schema/12_storage-channel";
|
||||
|
||||
export type OrganizationTabsProps = {
|
||||
organization: OrganizationWithMembers;
|
||||
notificationChannels: NotificationChannel[];
|
||||
storageChannels: StorageChannel[];
|
||||
activeMember: MemberWithUser
|
||||
};
|
||||
|
||||
export const OrganizationTabs = ({activeMember, organization, notificationChannels}: OrganizationTabsProps) => {
|
||||
export const OrganizationTabs = ({activeMember, organization, notificationChannels, storageChannels}: OrganizationTabsProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -28,6 +33,7 @@ export const OrganizationTabs = ({activeMember, organization, notificationChanne
|
||||
const {
|
||||
canManageUsers,
|
||||
canManageNotifications,
|
||||
canManageStorages
|
||||
} = useOrganizationPermissions(activeMember);
|
||||
|
||||
|
||||
@@ -43,7 +49,7 @@ export const OrganizationTabs = ({activeMember, organization, notificationChanne
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
{canManageNotifications ?
|
||||
{(canManageNotifications && canManageStorages) ?
|
||||
<Tabs className="h-full" value={tab} onValueChange={handleChangeTab}>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger
|
||||
@@ -59,6 +65,12 @@ export const OrganizationTabs = ({activeMember, organization, notificationChanne
|
||||
>
|
||||
Notifiers
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
className="w-full"
|
||||
value="storages"
|
||||
>
|
||||
Storages
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="h-full" value="users">
|
||||
<SettingsOrganizationMembersTable organization={organization}/>
|
||||
@@ -69,6 +81,12 @@ export const OrganizationTabs = ({activeMember, organization, notificationChanne
|
||||
notificationChannels={notificationChannels}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent className="h-full" value="storages">
|
||||
<OrganizationStoragesTab
|
||||
organization={organization}
|
||||
storageChannels={storageChannels}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
:
|
||||
<SettingsOrganizationMembersTable organization={organization}/>
|
||||
|
||||
+3
-1
@@ -13,6 +13,7 @@ import * as organizationNotificationChannel from "./schema/09_notification-chann
|
||||
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 * as storagePolicy from "@/db/schema/13_storage-policy";
|
||||
|
||||
|
||||
import {Pool} from "pg";
|
||||
@@ -42,7 +43,8 @@ export const schemas = {
|
||||
...organizationNotificationChannel,
|
||||
...alertPolicy,
|
||||
...notificationLog,
|
||||
...storageChannel
|
||||
...storageChannel,
|
||||
...storagePolicy
|
||||
};
|
||||
|
||||
export const db = drizzle({
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
CREATE TYPE "public"."backup_storage_status" AS ENUM('pending', 'success', 'failed');--> statement-breakpoint
|
||||
CREATE TABLE "backup_storage" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"backup_id" uuid NOT NULL,
|
||||
"storage_channel_id" uuid NOT NULL,
|
||||
"status" "backup_storage_status" DEFAULT 'pending' NOT NULL,
|
||||
"path" text,
|
||||
"size" integer,
|
||||
"checksum" text,
|
||||
"updated_at" timestamp,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"deleted_at" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "backup_storage" ADD CONSTRAINT "backup_storage_backup_id_backups_id_fk" FOREIGN KEY ("backup_id") REFERENCES "public"."backups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "backup_storage" ADD CONSTRAINT "backup_storage_storage_channel_id_storage_channel_id_fk" FOREIGN KEY ("storage_channel_id") REFERENCES "public"."storage_channel"("id") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "storage_channel" ADD COLUMN "organization_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "storage_channel" ADD CONSTRAINT "storage_channel_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "notification_channel" ADD COLUMN "organization_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "notification_channel" ADD CONSTRAINT "notification_channel_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -155,6 +155,27 @@
|
||||
"when": 1768337434929,
|
||||
"tag": "0021_soft_blockbuster",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 22,
|
||||
"version": "7",
|
||||
"when": 1768379186747,
|
||||
"tag": "0022_purple_retro_girl",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 23,
|
||||
"version": "7",
|
||||
"when": 1768412730102,
|
||||
"tag": "0023_common_the_captain",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 24,
|
||||
"version": "7",
|
||||
"when": 1768412750113,
|
||||
"tag": "0024_lush_blindfold",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {z} from "zod";
|
||||
import {timestamps} from "@/db/schema/00_common";
|
||||
import {AlertPolicy, alertPolicy} from "@/db/schema/10_alert-policy";
|
||||
import {StoragePolicy, storagePolicy} from "@/db/schema/13_storage-policy";
|
||||
import {backupStorage} from "@/db/schema/14_storage-backup";
|
||||
|
||||
export const database = pgTable("databases", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
@@ -41,7 +42,6 @@ export const backup = pgTable(
|
||||
.references(() => database.id, {onDelete: "cascade"}),
|
||||
...timestamps
|
||||
},
|
||||
// (table) => [uniqueIndex("database_id_status_unique").on(table.databaseId, table.status)]
|
||||
);
|
||||
|
||||
export const retentionPolicyType = pgEnum("retention_policy_type", ["count", "days", "gfs"]);
|
||||
@@ -87,6 +87,7 @@ export const databaseRelations = relations(database, ({one, many}) => ({
|
||||
export const backupRelations = relations(backup, ({one, many}) => ({
|
||||
database: one(database, {fields: [backup.databaseId], references: [database.id]}),
|
||||
restorations: many(restoration),
|
||||
storages: many(backupStorage),
|
||||
}));
|
||||
|
||||
export const restorationRelations = relations(restoration, ({one}) => ({
|
||||
|
||||
@@ -12,6 +12,7 @@ export const providerKindEnum = pgEnum('provider_kind', ['slack', 'smtp', 'disco
|
||||
export const notificationChannel = pgTable('notification_channel', {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
provider: providerKindEnum('provider').notNull(),
|
||||
organizationId: uuid("organization_id").references(() => organization.id, {onDelete: "cascade"}),
|
||||
name: varchar('name', {length: 255}).notNull(),
|
||||
config: jsonb('config').notNull(),
|
||||
enabled: boolean('enabled').default(false).notNull(),
|
||||
|
||||
@@ -4,12 +4,14 @@ import {organization} from "@/db/schema/03_organization";
|
||||
import {relations} from "drizzle-orm";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
import {database} from "@/db/schema/07_database";
|
||||
|
||||
|
||||
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3']);
|
||||
|
||||
export const storageChannel = pgTable('storage_channel', {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
organizationId: uuid("organization_id").references(() => organization.id, {onDelete: "cascade"}),
|
||||
provider: providerStorageKindEnum('provider').notNull(),
|
||||
name: varchar('name', {length: 255}).notNull(),
|
||||
config: jsonb('config').notNull(),
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { pgTable, uuid, text, integer, pgEnum } from "drizzle-orm/pg-core";
|
||||
import { timestamps } from "@/db/schema/00_common";
|
||||
import { storageChannel } from "@/db/schema/12_storage-channel";
|
||||
import {backup} from "@/db/schema/07_database";
|
||||
import {relations} from "drizzle-orm";
|
||||
|
||||
export const backupStorageStatusEnum = pgEnum("backup_storage_status", [
|
||||
"pending",
|
||||
"success",
|
||||
"failed",
|
||||
]);
|
||||
|
||||
export const backupStorage = pgTable("backup_storage", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
backupId: uuid("backup_id")
|
||||
.notNull()
|
||||
.references(() => backup.id, { onDelete: "cascade" }),
|
||||
storageChannelId: uuid("storage_channel_id")
|
||||
.notNull()
|
||||
.references(() => storageChannel.id, { onDelete: "cascade" }),
|
||||
status: backupStorageStatusEnum("status").notNull().default("pending"),
|
||||
path: text("path"),
|
||||
size: integer("size"),
|
||||
checksum: text("checksum"),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
|
||||
export const backupStorageRelations = relations(backupStorage, ({ one }) => ({
|
||||
backup: one(backup, {
|
||||
fields: [backupStorage.backupId],
|
||||
references: [backup.id],
|
||||
}),
|
||||
storageChannel: one(storageChannel, {
|
||||
fields: [backupStorage.storageChannelId],
|
||||
references: [storageChannel.id],
|
||||
}),
|
||||
}));
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
notificationChannel,
|
||||
organizationNotificationChannel
|
||||
} from "@/db/schema/09_notification-channel";
|
||||
import {storageChannel} from "@/db/schema/12_storage-channel";
|
||||
|
||||
export async function getOrganizationChannels(organizationId: string) {
|
||||
return await db
|
||||
@@ -17,6 +18,7 @@ export async function getOrganizationChannels(organizationId: string) {
|
||||
updatedAt: notificationChannel.updatedAt,
|
||||
createdAt: notificationChannel.createdAt,
|
||||
deletedAt: notificationChannel.deletedAt,
|
||||
organizationId: notificationChannel.organizationId
|
||||
})
|
||||
.from(organizationNotificationChannel)
|
||||
.innerJoin(
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import {desc, eq} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import {organizationStorageChannel, StorageChannel, storageChannel} from "@/db/schema/12_storage-channel";
|
||||
|
||||
export async function getOrganizationStorageChannels(organizationId: string) {
|
||||
return await db
|
||||
.select({
|
||||
id: storageChannel.id,
|
||||
name: storageChannel.name,
|
||||
provider: storageChannel.provider,
|
||||
organizationId: storageChannel.organizationId,
|
||||
config: storageChannel.config,
|
||||
enabled: storageChannel.enabled,
|
||||
updatedAt: storageChannel.updatedAt,
|
||||
createdAt: storageChannel.createdAt,
|
||||
deletedAt: storageChannel.deletedAt,
|
||||
})
|
||||
.from(organizationStorageChannel)
|
||||
.innerJoin(
|
||||
storageChannel,
|
||||
eq(organizationStorageChannel.storageChannelId, storageChannel.id)
|
||||
)
|
||||
.orderBy(desc(storageChannel.createdAt))
|
||||
.where(eq(organizationStorageChannel.organizationId, organizationId)) as unknown as StorageChannel[];
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export type OrganizationPermissions = {
|
||||
canManageSettings: boolean;
|
||||
canManageUsers: boolean;
|
||||
canManageNotifications: boolean;
|
||||
canManageStorages: boolean;
|
||||
canManageDangerZone: boolean;
|
||||
};
|
||||
|
||||
@@ -32,6 +33,7 @@ export const computeOrganizationPermissions = (
|
||||
canManageSettings: isOwner || isAdmin,
|
||||
canManageUsers: isOwner || isAdmin,
|
||||
canManageNotifications: isOwner || isAdmin,
|
||||
canManageStorages: isOwner || isAdmin,
|
||||
canManageDangerZone: isOwner,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user