fix: set default notification channel

This commit is contained in:
charles-gauthereau
2026-03-26 22:45:42 +01:00
parent c3ae82fde7
commit c095d393cf
14 changed files with 141 additions and 15639 deletions
@@ -14,6 +14,8 @@ export default async function RoutePage(props: PageParams<{}>) {
where: (fields, {eq}) => eq(fields.name, "system"),
});
console.log(settings)
const storageChannels = await db.query.storageChannel.findMany({
with: {
organizations: true
+3
View File
@@ -43,6 +43,9 @@ const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: true,
},
logging: {
browserToTerminal: false,
},
experimental: {
serverActions: {
bodySizeLimit: "10gb",
+27 -28
View File
@@ -15,8 +15,8 @@
"release": "release-it"
},
"dependencies": {
"@better-auth/passkey": "^1.4.19",
"@better-auth/sso": "^1.4.19",
"@better-auth/passkey": "^1.5.6",
"@better-auth/sso": "^1.5.6",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
@@ -47,20 +47,20 @@
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-email/components": "^0.0.41",
"@t3-oss/env-nextjs": "^0.13.10",
"@tanstack/react-query": "^5.90.21",
"@t3-oss/env-nextjs": "^0.13.11",
"@tanstack/react-query": "^5.95.2",
"@tanstack/react-table": "^8.21.3",
"@types/nodemailer": "^6.4.23",
"@types/ws": "^8.18.1",
"@zenstackhq/runtime": "2.14.2",
"argon2": "^0.43.1",
"bcrypt": "^6.0.0",
"better-auth": "1.4.18",
"better-auth": "1.5.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dockerode": "^4.0.9",
"dockerode": "^4.0.10",
"dotenv": "^16.6.1",
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1",
@@ -68,23 +68,23 @@
"googleapis": "^170.1.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.553.0",
"minio": "^8.0.6",
"motion": "^12.34.3",
"next": "16.1.5",
"minio": "^8.0.7",
"motion": "^12.38.0",
"next": "16.2.1",
"next-safe-action": "^7.10.8",
"next-themes": "^0.4.6",
"node-cron": "^4.2.1",
"node-forge": "^1.3.3",
"node-forge": "^1.4.0",
"nodemailer": "^7.0.13",
"npm-check-updates": "^18.3.1",
"pg": "^8.18.0",
"pg": "^8.20.0",
"prettier": "^3.8.1",
"react": "^19.2.4",
"react-day-picker": "9.7.0",
"react-dom": "^19.2.4",
"react-dropzone": "^14.4.1",
"react-email": "^4.3.2",
"react-hook-form": "^7.71.2",
"react-hook-form": "^7.72.0",
"react-qr-code": "^2.0.18",
"react-resizable-panels": "^3.0.6",
"react-twc": "^1.5.1",
@@ -94,11 +94,11 @@
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"sonner": "^2.0.7",
"swiper": "^12.1.2",
"swiper": "^12.1.3",
"tailwind-merge": "^3.5.0",
"uuid": "^11.1.0",
"vaul": "^1.1.2",
"ws": "^8.19.0",
"ws": "^8.20.0",
"zod": "^3.25.76"
},
"devDependencies": {
@@ -107,30 +107,29 @@
"@react-email/preview-server": "4.3.2",
"@react-email/render": "^2.0.4",
"@release-it/bumper": "^7.0.5",
"@release-it/conventional-changelog": "^10.0.5",
"@tailwindcss/postcss": "^4.2.1",
"@release-it/conventional-changelog": "^10.0.6",
"@tailwindcss/postcss": "^4.2.2",
"@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/node": "^22.19.11",
"@types/node": "^22.19.15",
"@types/node-forge": "^1.3.14",
"@types/pg": "^8.16.0",
"@types/pg": "^8.20.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@zenstackhq/openapi": "^2.22.1",
"@zenstackhq/tanstack-query": "^2.22.2",
"baseline-browser-mapping": "^2.10.0",
"drizzle-kit": "^0.31.9",
"esbuild": "^0.27.3",
"eslint": "^9.39.3",
"eslint-config-next": "^16.1.6",
"baseline-browser-mapping": "^2.10.11",
"drizzle-kit": "^0.31.10",
"esbuild": "^0.27.4",
"eslint": "^9.39.4",
"eslint-config-next": "^16.2.1",
"eslint-plugin-tailwindcss": "^3.18.2",
"framer-motion": "^12.34.3",
"postcss": "^8.5.6",
"framer-motion": "^12.38.0",
"postcss": "^8.5.8",
"release-it": "^19.2.4",
"tailwindcss": "^4.2.1",
"tailwindcss": "^4.2.2",
"tsx": "^4.21.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"zenstack": "2.14.2"
},
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a"
}
}
-15531
View File
File diff suppressed because it is too large Load Diff
+13 -2
View File
@@ -209,11 +209,22 @@ type UseZodFormProps<Z extends ZodSchema> = Exclude<
};
// const useZodForm = <Z extends ZodSchema>({
// schema,
// ...formProps
// }: UseZodFormProps<Z>) =>
// useForm({
// ...formProps,
// // @ts-ignore
// resolver: zodResolver(schema),
// });
const useZodForm = <Z extends ZodSchema>({
schema,
...formProps
}: UseZodFormProps<Z>) =>
useForm({
}: UseZodFormProps<Z>): UseFormReturn<TypeOf<Z>> =>
useForm<TypeOf<Z>>({
...formProps,
// @ts-ignore
resolver: zodResolver(schema),
@@ -1,6 +1,5 @@
"use client";
import {Card, CardContent} from "@/components/ui/card";
import {
FormControl,
FormDescription,
@@ -1,6 +1,6 @@
"use client"
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {Info} from "lucide-react";
import {Info, Send} from "lucide-react";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {useRouter} from "next/navigation";
import {Setting} from "@/db/schema/01_setting";
@@ -22,6 +22,7 @@ import {
updateNotificationSettingsAction
} from "@/components/wrappers/dashboard/admin/settings/notification/settings-notification.action";
import {toast} from "sonner";
import {Badge} from "@/components/ui/badge";
export type SettingsNotificationSectionProps = {
settings: Setting;
@@ -34,16 +35,14 @@ export const SettingsNotificationSection = ({settings, notificationChannels}: Se
const form = useZodForm({
schema: DefaultNotificationSchema,
defaultValues: {
notificationChannelId: settings.defaultNotificationChannelId ?? undefined,
notificationChannelId: settings.defaultNotificationChannelId ?? "",
}
});
const mutation = useMutation({
mutationFn: async (values: DefaultNotificationType) => {
const result = await updateNotificationSettingsAction({name: "system", data: values})
const inner = result?.data;
if (inner?.success) {
toast.success(inner.actionSuccess?.message);
router.refresh();
@@ -53,14 +52,21 @@ export const SettingsNotificationSection = ({settings, notificationChannels}: Se
}
});
return (
<div className="flex flex-col h-full">
<Alert className="mt-3">
<Info className="h-4 w-4"/>
<Alert className="mt-3 flex items-start gap-2">
<Info className="h-4 w-4 mt-1"/>
<div>
<AlertTitle>Informations</AlertTitle>
<AlertDescription>
The default notification channel will be used to send agent health alert notifications, and will be applied by default if no notification policy is defined at the database or organization level.</AlertDescription>
<AlertDescription className="flex flex-wrap items-center gap-1">
The default notification channel will be used to send
<Badge>error_health_agent</Badge>
<Badge>error_health_database</Badge>
<Badge>error_backup</Badge>
<Badge>error_restore</Badge>
events. For more options like notify when success, please set policy at database level
</AlertDescription>
</div>
</Alert>
<div className="flex flex-col h-full py-4 gap-3">
<Form
@@ -75,12 +81,15 @@ export const SettingsNotificationSection = ({settings, notificationChannels}: Se
control={form.control}
name="notificationChannelId"
render={({ field }) => (
<FormItem className="flex-grow min-w-[200px] sm:flex-grow-0 sm:w-64">
<FormItem className="flex-grow ">
<FormLabel>Default Notification Provider</FormLabel>
{notificationChannels.length === 0 ? (
<div className="text-sm text-muted-foreground">No channel available</div>
) : (
<Select value={field.value} onValueChange={field.onChange}>
<Select
value={field.value ?? ""}
onValueChange={(value) => field.onChange(value)}
>
<SelectTrigger className="w-full h-full mb-0">
<SelectValue placeholder="Select a default channel" />
</SelectTrigger>
@@ -103,11 +112,36 @@ export const SettingsNotificationSection = ({settings, notificationChannels}: Se
)}
/>
</div>
{notificationChannels.length >0 && (
<ButtonWithLoading className="flex-shrink-0 w-full sm:w-auto" type="submit">
<div className="flex justify-between gap-4">
{notificationChannels.length > 0 && (
<ButtonWithLoading
type="submit"
>
Confirm
</ButtonWithLoading>
)}
<div className="flex justify-end">
{notificationChannels.length > 0 && form.getValues("notificationChannelId") ? (
<ButtonWithLoading
type="button"
variant="outline"
onClick={async () => {
form.setValue("notificationChannelId", "");
await mutation.mutateAsync({
notificationChannelId: null,
});
}}
className="flex-shrink-0 w-full sm:w-auto"
>
Reset
</ButtonWithLoading>
) : null}
</div>
</div>
</Form>
</div>
</div>
@@ -25,7 +25,7 @@ export const updateNotificationSettingsAction = userAction
const [updatedSettings] = await db
.update(drizzleDb.schemas.setting)
.set({
defaultNotificationChannelId: data.notificationChannelId,
defaultNotificationChannelId: data.notificationChannelId ?? null,
})
.where(eq(drizzleDb.schemas.setting.name, name))
.returning();
@@ -1,7 +1,7 @@
import {z} from "zod";
export const DefaultNotificationSchema = z.object({
notificationChannelId: z.string(),
notificationChannelId: z.string().optional().nullable()
});
export type DefaultNotificationType = z.infer<typeof DefaultNotificationSchema>;
@@ -109,7 +109,7 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
control={form.control}
name="storageChannelId"
render={({field}) => (
<FormItem className="flex-grow min-w-[200px] sm:flex-grow-0 sm:w-64">
<FormItem className="flex-grow">
<FormLabel>Default Storage Provider</FormLabel>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="w-full h-full mb-0">
@@ -37,7 +37,6 @@ import {backupOnly} from "@/components/wrappers/dashboard/projects/database/data
type ChannelPoliciesFormProps = {
onSuccess?: () => void;
channels: NotificationChannel[] | StorageChannel[];
organizationId: string;
database: DatabaseWith;
kind: ChannelKind
};
@@ -46,7 +45,6 @@ type ChannelPoliciesFormProps = {
export const ChannelPoliciesForm = ({
database,
channels,
organizationId,
onSuccess,
kind
}: ChannelPoliciesFormProps) => {
@@ -65,7 +65,6 @@ export const ChannelPoliciesModal = ({icon, kind, database, channels, organizati
</DialogDescription>
<Separator className="mt-3 mb-3"/>
<ChannelPoliciesForm
organizationId={organizationId}
channels={channels}
database={database}
onSuccess={() => setOpen(false)}
+2 -1
View File
@@ -1,10 +1,11 @@
import {boolean, pgTable, timestamp, uuid, varchar} from "drizzle-orm/pg-core";
import {boolean, pgTable, uuid, varchar} from "drizzle-orm/pg-core";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
import {timestamps} from "@/db/schema/00_common";
import {storageChannel} from "@/db/schema/12_storage-channel";
import {relations} from "drizzle-orm";
import {notificationChannel} from "@/db/schema/09_notification-channel";
import {eventKindEnum} from "@/db/schema/10_alert-policy";
export const setting = pgTable("settings", {
+35 -48
View File
@@ -45,7 +45,7 @@ export async function deleteHealthLogsOlderThan12h() {
}
//
//
// export async function sendNotificationsHealthCheck(event: EventKind) {
//
@@ -142,67 +142,54 @@ export async function deleteHealthLogsOlderThan12h() {
//
//
// export async function checkAgentsHealthError() {
//
// const agents = await db.query.agent.findMany({
// where: isNotNull(drizzleDb.schemas.agent.lastContact),
// });
//
// const settings = await db.query.setting.findFirst({
// where: (fields, { eq }) => eq(fields.name, "system"),
// });
//
// if (!settings) {
// throw new Error("System settings not found");
// }
//
// const now = new Date();
//
// for (const agent of agents) {
// const now = new Date()
// if (agent.lastContact - now > 10min) {
// if (!agent.lastContact) continue;
//
// if (agent.health_error_count < 4) {
// // peux pas depasser 3 count
// alors
// // update agent field health_error_count +1
//
// // send notifcations health_ping_fail
// const lastContactDate = new Date(agent.lastContact);
// const diffMinutes = (now.getTime() - lastContactDate.getTime()) / 1000 / 60;
//
// if (diffMinutes > 10) {
// if ((agent.health_error_count ?? 0) < 3) {
// await db.update(drizzleDb.schemas.agent)
// .set({
// health_error_count: (agent.health_error_count ?? 0) + 1,
// })
// .where(drizzleDb.schemas.agent.id.eq(agent.id));
//
// const payload: EventPayload = {
// title: titleMap[event],
// message,
// level: level,
// event: event,
// title: "Agent down",
// message: `Agent ${agent.name} is down`,
// level: "critical",
// event: "error_health_agent",
// data: {
// host: database.name,
// id: database.id,
// agentDatabaseId: database.agentDatabaseId,
// error,
// agent: agent.name,
// id: agent.id,
// error: "Agent is down",
// },
// };
//
//
// await dispatchNotification(payload, alertPolicy.id, undefined, undefined);
// }else {
// pass
// }
// await dispatchNotification(
// payload,
// undefined,
// settings.defaultNotificationChannelId,
// undefined
// );
// }
//
//
// if (!db.retentionPolicy) continue;
// await enforceRetention(db.id, db.retentionPolicy);
// }
//
//
//
// const now = new Date()
// const threshold = new Date(now.getTime() - 12 * 60 * 60 * 1000)
//
// const logsToDelete = await db
// .select()
// .from(drizzleDb.schemas.healthcheckLog)
// .where(
// lt(drizzleDb.schemas.healthcheckLog.date, threshold)
// )
//
// console.log(`Number of logs found to delete: ${logsToDelete.length}`)
//
// await db
// .delete(drizzleDb.schemas.healthcheckLog)
// .where(
// lt(drizzleDb.schemas.healthcheckLog.date, threshold)
// )
//
// return logsToDelete.length
// }
// }