feat: add generic webhook form & refactor each type in its own form (#239)
* feat: add generic webhook form & refactor each type in its own form * chore: pr feedbacks * fix(email-form): filter out empty emails
This commit is contained in:
parent
b0c397ab87
commit
658c42a53b
24 changed files with 1019 additions and 568 deletions
|
|
@ -2151,6 +2151,15 @@ export type GetScheduleNotificationsResponses = {
|
||||||
useTLS: boolean;
|
useTLS: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
} | {
|
||||||
|
method: 'GET' | 'POST';
|
||||||
|
type: 'generic';
|
||||||
|
url: string;
|
||||||
|
contentType?: string;
|
||||||
|
headers?: Array<string>;
|
||||||
|
messageKey?: string;
|
||||||
|
titleKey?: string;
|
||||||
|
useJson?: boolean;
|
||||||
} | {
|
} | {
|
||||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||||
topic: string;
|
topic: string;
|
||||||
|
|
@ -2184,7 +2193,7 @@ export type GetScheduleNotificationsResponses = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
destinationId: number;
|
destinationId: number;
|
||||||
|
|
@ -2241,6 +2250,15 @@ export type UpdateScheduleNotificationsResponses = {
|
||||||
useTLS: boolean;
|
useTLS: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
} | {
|
||||||
|
method: 'GET' | 'POST';
|
||||||
|
type: 'generic';
|
||||||
|
url: string;
|
||||||
|
contentType?: string;
|
||||||
|
headers?: Array<string>;
|
||||||
|
messageKey?: string;
|
||||||
|
titleKey?: string;
|
||||||
|
useJson?: boolean;
|
||||||
} | {
|
} | {
|
||||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||||
topic: string;
|
topic: string;
|
||||||
|
|
@ -2274,7 +2292,7 @@ export type UpdateScheduleNotificationsResponses = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
destinationId: number;
|
destinationId: number;
|
||||||
|
|
@ -2568,6 +2586,15 @@ export type ListNotificationDestinationsResponses = {
|
||||||
useTLS: boolean;
|
useTLS: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
} | {
|
||||||
|
method: 'GET' | 'POST';
|
||||||
|
type: 'generic';
|
||||||
|
url: string;
|
||||||
|
contentType?: string;
|
||||||
|
headers?: Array<string>;
|
||||||
|
messageKey?: string;
|
||||||
|
titleKey?: string;
|
||||||
|
useJson?: boolean;
|
||||||
} | {
|
} | {
|
||||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||||
topic: string;
|
topic: string;
|
||||||
|
|
@ -2601,7 +2628,7 @@ export type ListNotificationDestinationsResponses = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
@ -2629,6 +2656,15 @@ export type CreateNotificationDestinationData = {
|
||||||
useTLS: boolean;
|
useTLS: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
} | {
|
||||||
|
method: 'GET' | 'POST';
|
||||||
|
type: 'generic';
|
||||||
|
url: string;
|
||||||
|
contentType?: string;
|
||||||
|
headers?: Array<string>;
|
||||||
|
messageKey?: string;
|
||||||
|
titleKey?: string;
|
||||||
|
useJson?: boolean;
|
||||||
} | {
|
} | {
|
||||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||||
topic: string;
|
topic: string;
|
||||||
|
|
@ -2689,6 +2725,15 @@ export type CreateNotificationDestinationResponses = {
|
||||||
useTLS: boolean;
|
useTLS: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
} | {
|
||||||
|
method: 'GET' | 'POST';
|
||||||
|
type: 'generic';
|
||||||
|
url: string;
|
||||||
|
contentType?: string;
|
||||||
|
headers?: Array<string>;
|
||||||
|
messageKey?: string;
|
||||||
|
titleKey?: string;
|
||||||
|
useJson?: boolean;
|
||||||
} | {
|
} | {
|
||||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||||
topic: string;
|
topic: string;
|
||||||
|
|
@ -2722,7 +2767,7 @@ export type CreateNotificationDestinationResponses = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -2796,6 +2841,15 @@ export type GetNotificationDestinationResponses = {
|
||||||
useTLS: boolean;
|
useTLS: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
} | {
|
||||||
|
method: 'GET' | 'POST';
|
||||||
|
type: 'generic';
|
||||||
|
url: string;
|
||||||
|
contentType?: string;
|
||||||
|
headers?: Array<string>;
|
||||||
|
messageKey?: string;
|
||||||
|
titleKey?: string;
|
||||||
|
useJson?: boolean;
|
||||||
} | {
|
} | {
|
||||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||||
topic: string;
|
topic: string;
|
||||||
|
|
@ -2829,7 +2883,7 @@ export type GetNotificationDestinationResponses = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -2857,6 +2911,15 @@ export type UpdateNotificationDestinationData = {
|
||||||
useTLS: boolean;
|
useTLS: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
} | {
|
||||||
|
method: 'GET' | 'POST';
|
||||||
|
type: 'generic';
|
||||||
|
url: string;
|
||||||
|
contentType?: string;
|
||||||
|
headers?: Array<string>;
|
||||||
|
messageKey?: string;
|
||||||
|
titleKey?: string;
|
||||||
|
useJson?: boolean;
|
||||||
} | {
|
} | {
|
||||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||||
topic: string;
|
topic: string;
|
||||||
|
|
@ -2927,6 +2990,15 @@ export type UpdateNotificationDestinationResponses = {
|
||||||
useTLS: boolean;
|
useTLS: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
} | {
|
||||||
|
method: 'GET' | 'POST';
|
||||||
|
type: 'generic';
|
||||||
|
url: string;
|
||||||
|
contentType?: string;
|
||||||
|
headers?: Array<string>;
|
||||||
|
messageKey?: string;
|
||||||
|
titleKey?: string;
|
||||||
|
useJson?: boolean;
|
||||||
} | {
|
} | {
|
||||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||||
topic: string;
|
topic: string;
|
||||||
|
|
@ -2960,7 +3032,7 @@ export type UpdateNotificationDestinationResponses = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,19 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "~/client/components/ui/form";
|
} from "~/client/components/ui/form";
|
||||||
import { Input } from "~/client/components/ui/input";
|
import { Input } from "~/client/components/ui/input";
|
||||||
import { SecretInput } from "~/client/components/ui/secret-input";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
import { Checkbox } from "~/client/components/ui/checkbox";
|
|
||||||
import { notificationConfigSchemaBase } from "~/schemas/notifications";
|
import { notificationConfigSchemaBase } from "~/schemas/notifications";
|
||||||
|
import {
|
||||||
|
CustomForm,
|
||||||
|
DiscordForm,
|
||||||
|
EmailForm,
|
||||||
|
GenericForm,
|
||||||
|
GotifyForm,
|
||||||
|
NtfyForm,
|
||||||
|
PushoverForm,
|
||||||
|
SlackForm,
|
||||||
|
TelegramForm,
|
||||||
|
} from "./notification-forms";
|
||||||
|
|
||||||
export const formSchema = type({
|
export const formSchema = type({
|
||||||
name: "2<=string<=32",
|
name: "2<=string<=32",
|
||||||
|
|
@ -48,6 +57,9 @@ const defaultValuesForType = {
|
||||||
slack: {
|
slack: {
|
||||||
type: "slack" as const,
|
type: "slack" as const,
|
||||||
webhookUrl: "",
|
webhookUrl: "",
|
||||||
|
channel: "",
|
||||||
|
username: "",
|
||||||
|
iconEmoji: "",
|
||||||
},
|
},
|
||||||
discord: {
|
discord: {
|
||||||
type: "discord" as const,
|
type: "discord" as const,
|
||||||
|
|
@ -75,6 +87,16 @@ const defaultValuesForType = {
|
||||||
botToken: "",
|
botToken: "",
|
||||||
chatId: "",
|
chatId: "",
|
||||||
},
|
},
|
||||||
|
generic: {
|
||||||
|
type: "generic" as const,
|
||||||
|
url: "",
|
||||||
|
method: "POST" as const,
|
||||||
|
contentType: "application/json",
|
||||||
|
headers: [],
|
||||||
|
useJson: true,
|
||||||
|
titleKey: "title",
|
||||||
|
messageKey: "message",
|
||||||
|
},
|
||||||
custom: {
|
custom: {
|
||||||
type: "custom" as const,
|
type: "custom" as const,
|
||||||
shoutrrrUrl: "",
|
shoutrrrUrl: "",
|
||||||
|
|
@ -84,7 +106,9 @@ const defaultValuesForType = {
|
||||||
export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValues, formId, className }: Props) => {
|
export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValues, formId, className }: Props) => {
|
||||||
const form = useForm<NotificationFormValues>({
|
const form = useForm<NotificationFormValues>({
|
||||||
resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema),
|
resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema),
|
||||||
defaultValues: initialValues,
|
defaultValues: initialValues || {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
resetOptions: {
|
resetOptions: {
|
||||||
keepDefaultValues: true,
|
keepDefaultValues: true,
|
||||||
keepDirtyValues: false,
|
keepDirtyValues: false,
|
||||||
|
|
@ -97,7 +121,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialValues) {
|
if (!initialValues) {
|
||||||
form.reset({
|
form.reset({
|
||||||
name: form.getValues().name,
|
name: form.getValues().name || "",
|
||||||
...defaultValuesForType[watchedType as keyof typeof defaultValuesForType],
|
...defaultValuesForType[watchedType as keyof typeof defaultValuesForType],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -152,6 +176,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
|
||||||
<SelectItem value="ntfy">Ntfy</SelectItem>
|
<SelectItem value="ntfy">Ntfy</SelectItem>
|
||||||
<SelectItem value="pushover">Pushover</SelectItem>
|
<SelectItem value="pushover">Pushover</SelectItem>
|
||||||
<SelectItem value="telegram">Telegram</SelectItem>
|
<SelectItem value="telegram">Telegram</SelectItem>
|
||||||
|
<SelectItem value="generic">Generic Webhook</SelectItem>
|
||||||
<SelectItem value="custom">Custom (Shoutrrr URL)</SelectItem>
|
<SelectItem value="custom">Custom (Shoutrrr URL)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
@ -161,545 +186,15 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{watchedType === "email" && (
|
{watchedType === "email" && <EmailForm form={form} />}
|
||||||
<>
|
{watchedType === "slack" && <SlackForm form={form} />}
|
||||||
<FormField
|
{watchedType === "discord" && <DiscordForm form={form} />}
|
||||||
control={form.control}
|
{watchedType === "gotify" && <GotifyForm form={form} />}
|
||||||
name="smtpHost"
|
{watchedType === "ntfy" && <NtfyForm form={form} />}
|
||||||
render={({ field }) => (
|
{watchedType === "pushover" && <PushoverForm form={form} />}
|
||||||
<FormItem>
|
{watchedType === "telegram" && <TelegramForm form={form} />}
|
||||||
<FormLabel>SMTP Host</FormLabel>
|
{watchedType === "generic" && <GenericForm form={form} />}
|
||||||
<FormControl>
|
{watchedType === "custom" && <CustomForm form={form} />}
|
||||||
<Input {...field} placeholder="smtp.example.com" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="smtpPort"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>SMTP Port</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
type="number"
|
|
||||||
placeholder="587"
|
|
||||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Username (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="user@example.com" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<SecretInput {...field} placeholder="••••••••" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="from"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>From Address</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="noreply@example.com" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="to"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>To Addresses</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="user@example.com, admin@example.com"
|
|
||||||
value={Array.isArray(field.value) ? field.value.join(", ") : ""}
|
|
||||||
onChange={(e) => field.onChange(e.target.value.split(",").map((email) => email.trim()))}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Comma-separated list of recipient email addresses.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="useTLS"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center space-x-3">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
|
||||||
</FormControl>
|
|
||||||
<div className="space-y-1 leading-none">
|
|
||||||
<FormLabel>Use TLS</FormLabel>
|
|
||||||
<FormDescription>Enable TLS encryption for SMTP connection.</FormDescription>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{watchedType === "slack" && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="webhookUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Webhook URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Get this from your Slack app's Incoming Webhooks settings.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="channel"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Channel (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="#backups" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Override the default channel (use # for channels, @ for users).</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Bot Username (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="Zerobyte" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="iconEmoji"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Icon Emoji (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder=":floppy_disk:" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{watchedType === "discord" && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="webhookUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Webhook URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Get this from your Discord server's Integrations settings.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Bot Username (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="Zerobyte" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="avatarUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Avatar URL (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="https://example.com/avatar.png" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="threadId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Thread ID (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
ID of the thread to post messages in. Leave empty to post in the main channel.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{watchedType === "gotify" && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="serverUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Server URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="https://gotify.example.com" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Your self-hosted Gotify server URL.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="token"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>App Token</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<SecretInput {...field} placeholder="••••••••" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Application token from Gotify.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="priority"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Priority</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={10}
|
|
||||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Priority level (0-10, where 10 is highest).</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="path"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Path (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="/custom/path" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Custom path on the Gotify server, if applicable.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{watchedType === "ntfy" && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="serverUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Server URL (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="https://ntfy.example.com" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Leave empty to use ntfy.sh public service.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="topic"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Topic</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="ironmount-backups" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>The ntfy topic name to publish to.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Username (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="username" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Username for server authentication, if required.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<SecretInput {...field} placeholder="••••••••" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Password for server authentication, if required.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="accessToken"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Access token (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<SecretInput {...field} placeholder="••••••••" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Access token for server authentication. Will take precedence over username/password if set.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="priority"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Priority</FormLabel>
|
|
||||||
<Select onValueChange={field.onChange} defaultValue={String(field.value)} value={String(field.value)}>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select priority" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="max">Max (5)</SelectItem>
|
|
||||||
<SelectItem value="high">High (4)</SelectItem>
|
|
||||||
<SelectItem value="default">Default (3)</SelectItem>
|
|
||||||
<SelectItem value="low">Low (2)</SelectItem>
|
|
||||||
<SelectItem value="min">Min (1)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{watchedType === "pushover" && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="userKey"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>User Key</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Your Pushover user key from the dashboard.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="apiToken"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>API Token</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<SecretInput {...field} placeholder="••••••••" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Application API token from your Pushover application.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="devices"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Devices (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="iphone,android" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Comma-separated list of device names. Leave empty for all devices.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="priority"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Priority</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => field.onChange(Number(value))}
|
|
||||||
defaultValue={String(field.value)}
|
|
||||||
value={String(field.value)}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select priority" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="-1">Low (-1)</SelectItem>
|
|
||||||
<SelectItem value="0">Normal (0)</SelectItem>
|
|
||||||
<SelectItem value="1">High (1)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormDescription>Message priority level.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{watchedType === "telegram" && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="botToken"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Bot Token</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<SecretInput {...field} placeholder="123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Telegram bot token. Get this from BotFather when you create your bot.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="chatId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Chat ID</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="-1231234567890" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Telegram chat ID to send notifications to.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{watchedType === "custom" && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="shoutrrrUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Shoutrrr URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="smtp://user:pass@smtp.gmail.com:587/?from=you@gmail.com&to=recipient@example.com"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Direct Shoutrrr URL for power users. See
|
|
||||||
<a
|
|
||||||
href="https://shoutrrr.nickfedor.com/v0.12.0/services/overview/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-strong-accent hover:underline"
|
|
||||||
>
|
|
||||||
Shoutrrr documentation
|
|
||||||
</a>
|
|
||||||
for supported services and URL formats.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import type { NotificationFormValues } from "../create-notification-form";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: UseFormReturn<NotificationFormValues>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomForm = ({ form }: Props) => {
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="shoutrrrUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Shoutrrr URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="smtp://user:pass@smtp.gmail.com:587/?from=you@gmail.com&to=recipient@example.com"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Direct Shoutrrr URL for power users. See
|
||||||
|
<a
|
||||||
|
href="https://shoutrrr.nickfedor.com/v0.12.0/services/overview/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-strong-accent hover:underline"
|
||||||
|
>
|
||||||
|
Shoutrrr documentation
|
||||||
|
</a>
|
||||||
|
for supported services and URL formats.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import type { NotificationFormValues } from "../create-notification-form";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: UseFormReturn<NotificationFormValues>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DiscordForm = ({ form }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="webhookUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Get this from your Discord server's Integrations settings.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Bot Username (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="Zerobyte" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="avatarUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Avatar URL (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="https://example.com/avatar.png" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="threadId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Thread ID (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
ID of the thread to post messages in. Leave empty to post in the main channel.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||||
|
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||||
|
import type { NotificationFormValues } from "../create-notification-form";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: UseFormReturn<NotificationFormValues>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmailForm = ({ form }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="smtpHost"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>SMTP Host</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="smtp.example.com" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="smtpPort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>SMTP Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
placeholder="587"
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="user@example.com" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<SecretInput {...field} placeholder="••••••••" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="from"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>From Address</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="noreply@example.com" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="to"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>To Addresses</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="user@example.com, admin@example.com"
|
||||||
|
value={Array.isArray(field.value) ? field.value.join(", ") : ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
e.target.value
|
||||||
|
.split(",")
|
||||||
|
.map((email) => email.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Comma-separated list of recipient email addresses.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="useTLS"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center space-x-3">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>Use TLS</FormLabel>
|
||||||
|
<FormDescription>Enable TLS encryption for SMTP connection.</FormDescription>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
|
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||||
|
import { Textarea } from "~/client/components/ui/textarea";
|
||||||
|
import { CodeBlock } from "~/client/components/ui/code-block";
|
||||||
|
import { Label } from "~/client/components/ui/label";
|
||||||
|
import type { NotificationFormValues } from "../create-notification-form";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: UseFormReturn<NotificationFormValues>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WebhookPreview = ({ values }: { values: Partial<NotificationFormValues> }) => {
|
||||||
|
if (values.type !== "generic") return null;
|
||||||
|
|
||||||
|
const contentType = values.contentType || "application/json";
|
||||||
|
const headers = values.headers || [];
|
||||||
|
const useJson = values.useJson;
|
||||||
|
const titleKey = values.titleKey || "title";
|
||||||
|
const messageKey = values.messageKey || "message";
|
||||||
|
|
||||||
|
let body = "";
|
||||||
|
if (useJson) {
|
||||||
|
body = JSON.stringify(
|
||||||
|
{
|
||||||
|
[titleKey]: "Notification title",
|
||||||
|
[messageKey]: "Notification message",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
body = "Notification message";
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewCode = `${values.method} ${values.url}\nContent-Type: ${contentType}${headers.length > 0 ? `\n${headers.join("\n")}` : ""}
|
||||||
|
|
||||||
|
${body}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 pt-4 border-t">
|
||||||
|
<Label>Request Preview</Label>
|
||||||
|
<CodeBlock code={previewCode} filename="HTTP Request" />
|
||||||
|
<p className="text-[0.8rem] text-muted-foreground">This is a preview of the HTTP request that will be sent.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GenericForm = ({ form }: Props) => {
|
||||||
|
const watchedValues = form.watch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="url"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="https://api.example.com/webhook" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>The target URL for the webhook.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="method"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Method</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select method" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="GET">GET</SelectItem>
|
||||||
|
<SelectItem value="POST">POST</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="contentType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Content Type</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="application/json" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="headers"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Headers</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
placeholder="Authorization: Bearer token X-Custom-Header: value"
|
||||||
|
value={Array.isArray(field.value) ? field.value.join("\n") : ""}
|
||||||
|
onChange={(e) => field.onChange(e.target.value.split("\n"))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>One header per line in Key: Value format.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="useJson"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center space-x-3">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>Use JSON Template</FormLabel>
|
||||||
|
<FormDescription>Send the message as a JSON object.</FormDescription>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{form.watch("useJson") && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="titleKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Title Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="title" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>The JSON key for the notification title.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="messageKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Message Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="message" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>The JSON key for the notification message.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<WebhookPreview values={watchedValues} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||||
|
import type { NotificationFormValues } from "../create-notification-form";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: UseFormReturn<NotificationFormValues>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GotifyForm = ({ form }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serverUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="https://gotify.example.com" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Your self-hosted Gotify server URL.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="token"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>App Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<SecretInput {...field} placeholder="••••••••" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Application token from Gotify.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="priority"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Priority</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={10}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Priority level (0-10, where 10 is highest).</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="path"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Path (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="/custom/path" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Custom path on the Gotify server, if applicable.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export * from "./email-form";
|
||||||
|
export * from "./slack-form";
|
||||||
|
export * from "./discord-form";
|
||||||
|
export * from "./gotify-form";
|
||||||
|
export * from "./ntfy-form";
|
||||||
|
export * from "./pushover-form";
|
||||||
|
export * from "./telegram-form";
|
||||||
|
export * from "./generic-form";
|
||||||
|
export * from "./custom-form";
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
|
import type { NotificationFormValues } from "../create-notification-form";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: UseFormReturn<NotificationFormValues>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NtfyForm = ({ form }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serverUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server URL (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="https://ntfy.example.com" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Leave empty to use ntfy.sh public service.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="topic"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Topic</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="ironmount-backups" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>The ntfy topic name to publish to.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="username" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Username for server authentication, if required.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<SecretInput {...field} placeholder="••••••••" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Password for server authentication, if required.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="accessToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Access token (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<SecretInput {...field} placeholder="••••••••" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Access token for server authentication. Will take precedence over username/password if set.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="priority"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Priority</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} defaultValue={String(field.value)} value={String(field.value)}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select priority" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="max">Max (5)</SelectItem>
|
||||||
|
<SelectItem value="high">High (4)</SelectItem>
|
||||||
|
<SelectItem value="default">Default (3)</SelectItem>
|
||||||
|
<SelectItem value="low">Low (2)</SelectItem>
|
||||||
|
<SelectItem value="min">Min (1)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
|
import type { NotificationFormValues } from "../create-notification-form";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: UseFormReturn<NotificationFormValues>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PushoverForm = ({ form }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="userKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>User Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Your Pushover user key from the dashboard.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="apiToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<SecretInput {...field} placeholder="••••••••" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Application API token from your Pushover application.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="devices"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Devices (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="iphone,android" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Comma-separated list of device names. Leave empty for all devices.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="priority"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Priority</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => field.onChange(Number(value))}
|
||||||
|
defaultValue={String(field.value)}
|
||||||
|
value={String(field.value)}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select priority" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="-1">Low (-1)</SelectItem>
|
||||||
|
<SelectItem value="0">Normal (0)</SelectItem>
|
||||||
|
<SelectItem value="1">High (1)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>Message priority level.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import type { NotificationFormValues } from "../create-notification-form";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: UseFormReturn<NotificationFormValues>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SlackForm = ({ form }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="webhookUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Get this from your Slack app's Incoming Webhooks settings.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="channel"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Channel (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="#backups" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Override the default channel (use # for channels, @ for users).</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Bot Username (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="Zerobyte" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="iconEmoji"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Icon Emoji (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder=":floppy_disk:" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||||
|
import type { NotificationFormValues } from "../create-notification-form";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: UseFormReturn<NotificationFormValues>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TelegramForm = ({ form }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="botToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Bot Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<SecretInput {...field} placeholder="123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Telegram bot token. Get this from BotFather when you create your bot.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="chatId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Chat ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="-1231234567890" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Telegram chat ID to send notifications to.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -8,6 +8,7 @@ export const NOTIFICATION_TYPES = {
|
||||||
ntfy: "ntfy",
|
ntfy: "ntfy",
|
||||||
pushover: "pushover",
|
pushover: "pushover",
|
||||||
telegram: "telegram",
|
telegram: "telegram",
|
||||||
|
generic: "generic",
|
||||||
custom: "custom",
|
custom: "custom",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -72,6 +73,17 @@ export const telegramNotificationConfigSchema = type({
|
||||||
chatId: "string",
|
chatId: "string",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const genericNotificationConfigSchema = type({
|
||||||
|
type: "'generic'",
|
||||||
|
url: "string",
|
||||||
|
method: "'GET' | 'POST'",
|
||||||
|
contentType: "string?",
|
||||||
|
headers: "string[]?",
|
||||||
|
useJson: "boolean?",
|
||||||
|
titleKey: "string?",
|
||||||
|
messageKey: "string?",
|
||||||
|
});
|
||||||
|
|
||||||
export const customNotificationConfigSchema = type({
|
export const customNotificationConfigSchema = type({
|
||||||
type: "'custom'",
|
type: "'custom'",
|
||||||
shoutrrrUrl: "string",
|
shoutrrrUrl: "string",
|
||||||
|
|
@ -84,6 +96,7 @@ export const notificationConfigSchemaBase = emailNotificationConfigSchema
|
||||||
.or(ntfyNotificationConfigSchema)
|
.or(ntfyNotificationConfigSchema)
|
||||||
.or(pushoverNotificationConfigSchema)
|
.or(pushoverNotificationConfigSchema)
|
||||||
.or(telegramNotificationConfigSchema)
|
.or(telegramNotificationConfigSchema)
|
||||||
|
.or(genericNotificationConfigSchema)
|
||||||
.or(customNotificationConfigSchema);
|
.or(customNotificationConfigSchema);
|
||||||
|
|
||||||
export const notificationConfigSchema = notificationConfigSchemaBase.onUndeclaredKey("delete");
|
export const notificationConfigSchema = notificationConfigSchemaBase.onUndeclaredKey("delete");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { NotificationConfig } from "~/schemas/notifications";
|
import type { NotificationConfig } from "~/schemas/notifications";
|
||||||
|
|
||||||
export function buildCustomShoutrrrUrl(config: Extract<NotificationConfig, { type: "custom" }>): string {
|
export const buildCustomShoutrrrUrl = (config: Extract<NotificationConfig, { type: "custom" }>) => {
|
||||||
return config.shoutrrrUrl;
|
return config.shoutrrrUrl;
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NotificationConfig } from "~/schemas/notifications";
|
import type { NotificationConfig } from "~/schemas/notifications";
|
||||||
|
|
||||||
export function buildDiscordShoutrrrUrl(config: Extract<NotificationConfig, { type: "discord" }>): string {
|
export const buildDiscordShoutrrrUrl = (config: Extract<NotificationConfig, { type: "discord" }>) => {
|
||||||
const url = new URL(config.webhookUrl);
|
const url = new URL(config.webhookUrl);
|
||||||
const pathParts = url.pathname.split("/").filter(Boolean);
|
const pathParts = url.pathname.split("/").filter(Boolean);
|
||||||
|
|
||||||
|
|
@ -28,4 +28,4 @@ export function buildDiscordShoutrrrUrl(config: Extract<NotificationConfig, { ty
|
||||||
}
|
}
|
||||||
|
|
||||||
return shoutrrrUrl;
|
return shoutrrrUrl;
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NotificationConfig } from "~/schemas/notifications";
|
import type { NotificationConfig } from "~/schemas/notifications";
|
||||||
|
|
||||||
export function buildEmailShoutrrrUrl(config: Extract<NotificationConfig, { type: "email" }>): string {
|
export const buildEmailShoutrrrUrl = (config: Extract<NotificationConfig, { type: "email" }>) => {
|
||||||
const auth =
|
const auth =
|
||||||
config.username && config.password
|
config.username && config.password
|
||||||
? `${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@`
|
? `${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@`
|
||||||
|
|
@ -10,4 +10,4 @@ export function buildEmailShoutrrrUrl(config: Extract<NotificationConfig, { type
|
||||||
const useStartTLS = config.useTLS ? "yes" : "no";
|
const useStartTLS = config.useTLS ? "yes" : "no";
|
||||||
|
|
||||||
return `smtp://${auth}${host}/?from=${encodeURIComponent(config.from)}&to=${toRecipients}&starttls=${useStartTLS}`;
|
return `smtp://${auth}${host}/?from=${encodeURIComponent(config.from)}&to=${toRecipients}&starttls=${useStartTLS}`;
|
||||||
}
|
};
|
||||||
|
|
|
||||||
50
app/server/modules/notifications/builders/generic.ts
Normal file
50
app/server/modules/notifications/builders/generic.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import type { NotificationConfig } from "~/schemas/notifications";
|
||||||
|
|
||||||
|
export const buildGenericShoutrrrUrl = (config: Extract<NotificationConfig, { type: "generic" }>) => {
|
||||||
|
const targetUrl = new URL(config.url);
|
||||||
|
const shoutrrrUrl = new URL(`generic://${targetUrl.host}${targetUrl.pathname}`);
|
||||||
|
|
||||||
|
for (const [key, value] of targetUrl.searchParams.entries()) {
|
||||||
|
const reservedKeys = ["contenttype", "disabletls", "messagekey", "method", "template", "title", "titlekey"];
|
||||||
|
if (reservedKeys.includes(key.toLowerCase())) {
|
||||||
|
shoutrrrUrl.searchParams.append(`_${key}`, value);
|
||||||
|
} else {
|
||||||
|
shoutrrrUrl.searchParams.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUrl.protocol === "http:") {
|
||||||
|
shoutrrrUrl.searchParams.append("disabletls", "yes");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.method) {
|
||||||
|
shoutrrrUrl.searchParams.append("method", config.method);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.contentType) {
|
||||||
|
shoutrrrUrl.searchParams.append("contenttype", config.contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.useJson) {
|
||||||
|
shoutrrrUrl.searchParams.append("template", "json");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.titleKey) {
|
||||||
|
shoutrrrUrl.searchParams.append("titlekey", config.titleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.messageKey) {
|
||||||
|
shoutrrrUrl.searchParams.append("messagekey", config.messageKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.headers) {
|
||||||
|
for (const header of config.headers) {
|
||||||
|
const [key, ...valueParts] = header.split(":");
|
||||||
|
if (key && valueParts.length > 0) {
|
||||||
|
shoutrrrUrl.searchParams.append(`@${key.trim()}`, valueParts.join(":").trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shoutrrrUrl.toString();
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NotificationConfig } from "~/schemas/notifications";
|
import type { NotificationConfig } from "~/schemas/notifications";
|
||||||
|
|
||||||
export function buildGotifyShoutrrrUrl(config: Extract<NotificationConfig, { type: "gotify" }>): string {
|
export const buildGotifyShoutrrrUrl = (config: Extract<NotificationConfig, { type: "gotify" }>) => {
|
||||||
const url = new URL(config.serverUrl);
|
const url = new URL(config.serverUrl);
|
||||||
const hostname = url.hostname;
|
const hostname = url.hostname;
|
||||||
const port = url.port ? `:${url.port}` : "";
|
const port = url.port ? `:${url.port}` : "";
|
||||||
|
|
@ -13,4 +13,4 @@ export function buildGotifyShoutrrrUrl(config: Extract<NotificationConfig, { typ
|
||||||
}
|
}
|
||||||
|
|
||||||
return shoutrrrUrl;
|
return shoutrrrUrl;
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ import { buildGotifyShoutrrrUrl } from "./gotify";
|
||||||
import { buildNtfyShoutrrrUrl } from "./ntfy";
|
import { buildNtfyShoutrrrUrl } from "./ntfy";
|
||||||
import { buildPushoverShoutrrrUrl } from "./pushover";
|
import { buildPushoverShoutrrrUrl } from "./pushover";
|
||||||
import { buildTelegramShoutrrrUrl } from "./telegram";
|
import { buildTelegramShoutrrrUrl } from "./telegram";
|
||||||
|
import { buildGenericShoutrrrUrl } from "./generic";
|
||||||
import { buildCustomShoutrrrUrl } from "./custom";
|
import { buildCustomShoutrrrUrl } from "./custom";
|
||||||
|
|
||||||
export function buildShoutrrrUrl(config: NotificationConfig): string {
|
export const buildShoutrrrUrl = (config: NotificationConfig) => {
|
||||||
switch (config.type) {
|
switch (config.type) {
|
||||||
case "email":
|
case "email":
|
||||||
return buildEmailShoutrrrUrl(config);
|
return buildEmailShoutrrrUrl(config);
|
||||||
|
|
@ -24,12 +25,13 @@ export function buildShoutrrrUrl(config: NotificationConfig): string {
|
||||||
return buildPushoverShoutrrrUrl(config);
|
return buildPushoverShoutrrrUrl(config);
|
||||||
case "telegram":
|
case "telegram":
|
||||||
return buildTelegramShoutrrrUrl(config);
|
return buildTelegramShoutrrrUrl(config);
|
||||||
|
case "generic":
|
||||||
|
return buildGenericShoutrrrUrl(config);
|
||||||
case "custom":
|
case "custom":
|
||||||
return buildCustomShoutrrrUrl(config);
|
return buildCustomShoutrrrUrl(config);
|
||||||
default: {
|
default: {
|
||||||
// TypeScript exhaustiveness check
|
|
||||||
const _exhaustive: never = config;
|
const _exhaustive: never = config;
|
||||||
throw new Error(`Unsupported notification type: ${(_exhaustive as NotificationConfig).type}`);
|
throw new Error(`Unsupported notification type: ${(_exhaustive as NotificationConfig).type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NotificationConfig } from "~/schemas/notifications";
|
import type { NotificationConfig } from "~/schemas/notifications";
|
||||||
|
|
||||||
export function buildNtfyShoutrrrUrl(config: Extract<NotificationConfig, { type: "ntfy" }>): string {
|
export const buildNtfyShoutrrrUrl = (config: Extract<NotificationConfig, { type: "ntfy" }>) => {
|
||||||
let shoutrrrUrl: string;
|
let shoutrrrUrl: string;
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
@ -38,4 +38,4 @@ export function buildNtfyShoutrrrUrl(config: Extract<NotificationConfig, { type:
|
||||||
}
|
}
|
||||||
|
|
||||||
return shoutrrrUrl;
|
return shoutrrrUrl;
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NotificationConfig } from "~/schemas/notifications";
|
import type { NotificationConfig } from "~/schemas/notifications";
|
||||||
|
|
||||||
export function buildPushoverShoutrrrUrl(config: Extract<NotificationConfig, { type: "pushover" }>): string {
|
export const buildPushoverShoutrrrUrl = (config: Extract<NotificationConfig, { type: "pushover" }>) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (config.devices) {
|
if (config.devices) {
|
||||||
|
|
@ -19,4 +19,4 @@ export function buildPushoverShoutrrrUrl(config: Extract<NotificationConfig, { t
|
||||||
}
|
}
|
||||||
|
|
||||||
return shoutrrrUrl;
|
return shoutrrrUrl;
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NotificationConfig } from "~/schemas/notifications";
|
import type { NotificationConfig } from "~/schemas/notifications";
|
||||||
|
|
||||||
export function buildSlackShoutrrrUrl(config: Extract<NotificationConfig, { type: "slack" }>): string {
|
export const buildSlackShoutrrrUrl = (config: Extract<NotificationConfig, { type: "slack" }>) => {
|
||||||
const url = new URL(config.webhookUrl);
|
const url = new URL(config.webhookUrl);
|
||||||
const pathParts = url.pathname.split("/").filter(Boolean);
|
const pathParts = url.pathname.split("/").filter(Boolean);
|
||||||
|
|
||||||
|
|
@ -28,4 +28,4 @@ export function buildSlackShoutrrrUrl(config: Extract<NotificationConfig, { type
|
||||||
}
|
}
|
||||||
|
|
||||||
return shoutrrrUrl;
|
return shoutrrrUrl;
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { NotificationConfig } from "~/schemas/notifications";
|
import type { NotificationConfig } from "~/schemas/notifications";
|
||||||
|
|
||||||
export function buildTelegramShoutrrrUrl(config: Extract<NotificationConfig, { type: "telegram" }>): string {
|
export const buildTelegramShoutrrrUrl = (config: Extract<NotificationConfig, { type: "telegram" }>) => {
|
||||||
return `telegram://${config.botToken}@telegram?channels=${config.chatId}`;
|
return `telegram://${config.botToken}@telegram?channels=${config.chatId}`;
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ async function encryptSensitiveFields(config: NotificationConfig): Promise<Notif
|
||||||
...config,
|
...config,
|
||||||
botToken: await cryptoUtils.sealSecret(config.botToken),
|
botToken: await cryptoUtils.sealSecret(config.botToken),
|
||||||
};
|
};
|
||||||
|
case "generic":
|
||||||
|
return config;
|
||||||
case "custom":
|
case "custom":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
|
|
@ -118,6 +120,8 @@ async function decryptSensitiveFields(config: NotificationConfig): Promise<Notif
|
||||||
...config,
|
...config,
|
||||||
botToken: await cryptoUtils.resolveSecret(config.botToken),
|
botToken: await cryptoUtils.resolveSecret(config.botToken),
|
||||||
};
|
};
|
||||||
|
case "generic":
|
||||||
|
return config;
|
||||||
case "custom":
|
case "custom":
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue