mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-16 06:46:37 +00:00
Trying to use more consistant components
This commit is contained in:
@@ -1382,16 +1382,13 @@
|
||||
"alertingTriggerHcUnhealthy": "Health check unhealthy",
|
||||
"alertingSectionActions": "Actions",
|
||||
"alertingAddAction": "Add action",
|
||||
"alertingActionNotify": "Notify",
|
||||
"alertingActionSms": "SMS",
|
||||
"alertingActionNotify": "Email",
|
||||
"alertingActionWebhook": "Webhook",
|
||||
"alertingActionType": "Action type",
|
||||
"alertingNotifyUsers": "Users",
|
||||
"alertingNotifyRoles": "Roles",
|
||||
"alertingNotifyEmails": "Email addresses",
|
||||
"alertingEmailPlaceholder": "Add email and press Enter",
|
||||
"alertingSmsNumbers": "Phone numbers",
|
||||
"alertingSmsPlaceholder": "Add number and press Enter",
|
||||
"alertingWebhookMethod": "HTTP method",
|
||||
"alertingWebhookSecret": "Signing secret (optional)",
|
||||
"alertingWebhookSecretPlaceholder": "HMAC secret",
|
||||
@@ -1416,8 +1413,6 @@
|
||||
"alertingErrorTriggerSite": "Choose a site trigger",
|
||||
"alertingErrorTriggerHealth": "Choose a health check trigger",
|
||||
"alertingErrorNotifyRecipients": "Pick users, roles, or at least one email",
|
||||
"alertingErrorSmsPhones": "Add at least one phone number",
|
||||
"alertingErrorWebhookUrl": "Enter a valid webhook URL",
|
||||
"alertingConfigureSource": "Configure Source",
|
||||
"alertingConfigureTrigger": "Configure Trigger",
|
||||
"alertingConfigureActions": "Configure Actions",
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@app/components/ui/select";
|
||||
import { TagInput } from "@app/components/tags/tag-input";
|
||||
import { TagInput, type Tag } from "@app/components/tags/tag-input";
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import {
|
||||
type AlertRuleFormAction,
|
||||
@@ -46,9 +46,9 @@ import { useFormContext, useWatch } from "react-hook-form";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
export function DropdownAddAction({
|
||||
onPick
|
||||
onAdd
|
||||
}: {
|
||||
onPick: (type: "notify" | "sms" | "webhook") => void;
|
||||
onAdd: (type: AlertRuleFormAction["type"]) => void;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -58,44 +58,32 @@ export function DropdownAddAction({
|
||||
<Button type="button" variant="outline" size="sm">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
{t("alertingAddAction")}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-52 p-2" align="end">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="justify-start"
|
||||
onClick={() => {
|
||||
onPick("notify");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("alertingActionNotify")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="justify-start"
|
||||
onClick={() => {
|
||||
onPick("sms");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("alertingActionSms")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="justify-start"
|
||||
onClick={() => {
|
||||
onPick("webhook");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("alertingActionWebhook")}
|
||||
</Button>
|
||||
</div>
|
||||
<PopoverContent className="p-0 w-48" align="start">
|
||||
<Command>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
onAdd("notify");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("alertingActionNotify")}
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
onAdd("webhook");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("alertingActionWebhook")}
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
@@ -325,15 +313,10 @@ export function ActionBlock({
|
||||
if (nt === "notify") {
|
||||
form.setValue(`actions.${index}`, {
|
||||
type: "notify",
|
||||
userIds: [],
|
||||
roleIds: [],
|
||||
userTags: [],
|
||||
roleTags: [],
|
||||
emailTags: []
|
||||
});
|
||||
} else if (nt === "sms") {
|
||||
form.setValue(`actions.${index}`, {
|
||||
type: "sms",
|
||||
phoneTags: []
|
||||
});
|
||||
} else {
|
||||
form.setValue(`actions.${index}`, {
|
||||
type: "webhook",
|
||||
@@ -354,9 +337,6 @@ export function ActionBlock({
|
||||
<SelectItem value="notify">
|
||||
{t("alertingActionNotify")}
|
||||
</SelectItem>
|
||||
<SelectItem value="sms">
|
||||
{t("alertingActionSms")}
|
||||
</SelectItem>
|
||||
<SelectItem value="webhook">
|
||||
{t("alertingActionWebhook")}
|
||||
</SelectItem>
|
||||
@@ -373,9 +353,6 @@ export function ActionBlock({
|
||||
form={form}
|
||||
/>
|
||||
)}
|
||||
{type === "sms" && (
|
||||
<SmsActionFields index={index} control={control} form={form} />
|
||||
)}
|
||||
{type === "webhook" && (
|
||||
<WebhookActionFields
|
||||
index={index}
|
||||
@@ -399,41 +376,124 @@ function NotifyActionFields({
|
||||
form: UseFormReturn<AlertRuleFormValues>;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
|
||||
const [emailActiveIdx, setEmailActiveIdx] = useState<number | null>(null);
|
||||
const userIds = form.watch(`actions.${index}.userIds`) ?? [];
|
||||
const roleIds = form.watch(`actions.${index}.roleIds`) ?? [];
|
||||
const emailTags = form.watch(`actions.${index}.emailTags`) ?? [];
|
||||
const [activeUsersTagIndex, setActiveUsersTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const { data: orgUsers = [] } = useQuery(orgQueries.users({ orgId }));
|
||||
const { data: orgRoles = [] } = useQuery(orgQueries.roles({ orgId }));
|
||||
|
||||
const allUsers = useMemo(
|
||||
() =>
|
||||
orgUsers.map((u) => ({
|
||||
id: String(u.id),
|
||||
text: getUserDisplayName({
|
||||
email: u.email,
|
||||
name: u.name,
|
||||
username: u.username
|
||||
})
|
||||
})),
|
||||
[orgUsers]
|
||||
);
|
||||
|
||||
const allRoles = useMemo(
|
||||
() =>
|
||||
orgRoles
|
||||
.map((r) => ({ id: String(r.roleId), text: r.name }))
|
||||
.filter((r) => r.text !== "Admin"),
|
||||
[orgRoles]
|
||||
);
|
||||
|
||||
const userTags = (form.watch(`actions.${index}.userTags`) ?? []) as Tag[];
|
||||
const roleTags = (form.watch(`actions.${index}.roleTags`) ?? []) as Tag[];
|
||||
const emailTags = (form.watch(`actions.${index}.emailTags`) ?? []) as Tag[];
|
||||
|
||||
return (
|
||||
<div className="space-y-3 pt-1">
|
||||
<FormItem>
|
||||
<FormLabel>{t("alertingNotifyUsers")}</FormLabel>
|
||||
<UserMultiSelect
|
||||
orgId={orgId}
|
||||
value={userIds}
|
||||
onChange={(ids) =>
|
||||
form.setValue(`actions.${index}.userIds`, ids)
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<FormLabel>{t("alertingNotifyRoles")}</FormLabel>
|
||||
<RoleMultiSelect
|
||||
orgId={orgId}
|
||||
value={roleIds}
|
||||
onChange={(ids) =>
|
||||
form.setValue(`actions.${index}.roleIds`, ids)
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormField
|
||||
control={control}
|
||||
name={`actions.${index}.userTags`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>{t("alertingNotifyUsers")}</FormLabel>
|
||||
<FormControl>
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={activeUsersTagIndex}
|
||||
setActiveTagIndex={setActiveUsersTagIndex}
|
||||
placeholder={t("alertingSelectUsers")}
|
||||
size="sm"
|
||||
tags={userTags}
|
||||
setTags={(newTags) => {
|
||||
const next =
|
||||
typeof newTags === "function"
|
||||
? newTags(userTags)
|
||||
: newTags;
|
||||
form.setValue(
|
||||
`actions.${index}.userTags`,
|
||||
next as Tag[]
|
||||
);
|
||||
}}
|
||||
enableAutocomplete={true}
|
||||
autocompleteOptions={allUsers}
|
||||
allowDuplicates={false}
|
||||
restrictTagsToAutocompleteOptions={true}
|
||||
sortTags={true}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name={`actions.${index}.roleTags`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>{t("alertingNotifyRoles")}</FormLabel>
|
||||
<FormControl>
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={activeRolesTagIndex}
|
||||
setActiveTagIndex={setActiveRolesTagIndex}
|
||||
placeholder={t("alertingSelectRoles")}
|
||||
size="sm"
|
||||
tags={roleTags}
|
||||
setTags={(newTags) => {
|
||||
const next =
|
||||
typeof newTags === "function"
|
||||
? newTags(roleTags)
|
||||
: newTags;
|
||||
form.setValue(
|
||||
`actions.${index}.roleTags`,
|
||||
next as Tag[]
|
||||
);
|
||||
}}
|
||||
enableAutocomplete={true}
|
||||
autocompleteOptions={allRoles}
|
||||
allowDuplicates={false}
|
||||
restrictTagsToAutocompleteOptions={true}
|
||||
sortTags={true}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name={`actions.${index}.emailTags`}
|
||||
render={() => (
|
||||
<FormItem>
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>{t("alertingNotifyEmails")}</FormLabel>
|
||||
<FormControl>
|
||||
<TagInput
|
||||
{...field}
|
||||
tags={emailTags}
|
||||
setTags={(updater) => {
|
||||
const next =
|
||||
@@ -442,12 +502,18 @@ function NotifyActionFields({
|
||||
: updater;
|
||||
form.setValue(
|
||||
`actions.${index}.emailTags`,
|
||||
next
|
||||
next as Tag[]
|
||||
);
|
||||
}}
|
||||
activeTagIndex={emailActiveIdx}
|
||||
setActiveTagIndex={setEmailActiveIdx}
|
||||
placeholder={t("alertingEmailPlaceholder")}
|
||||
size="sm"
|
||||
allowDuplicates={false}
|
||||
sortTags={true}
|
||||
validateTag={(tag) =>
|
||||
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(tag)
|
||||
}
|
||||
delimiterList={[",", "Enter"]}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -459,51 +525,6 @@ function NotifyActionFields({
|
||||
);
|
||||
}
|
||||
|
||||
function SmsActionFields({
|
||||
index,
|
||||
control,
|
||||
form
|
||||
}: {
|
||||
index: number;
|
||||
control: Control<AlertRuleFormValues>;
|
||||
form: UseFormReturn<AlertRuleFormValues>;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const [phoneActiveIdx, setPhoneActiveIdx] = useState<number | null>(null);
|
||||
const phoneTags = form.watch(`actions.${index}.phoneTags`) ?? [];
|
||||
return (
|
||||
<FormField
|
||||
control={control}
|
||||
name={`actions.${index}.phoneTags`}
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("alertingSmsNumbers")}</FormLabel>
|
||||
<FormControl>
|
||||
<TagInput
|
||||
tags={phoneTags}
|
||||
setTags={(updater) => {
|
||||
const next =
|
||||
typeof updater === "function"
|
||||
? updater(phoneTags)
|
||||
: updater;
|
||||
form.setValue(
|
||||
`actions.${index}.phoneTags`,
|
||||
next
|
||||
);
|
||||
}}
|
||||
activeTagIndex={phoneActiveIdx}
|
||||
setActiveTagIndex={setPhoneActiveIdx}
|
||||
placeholder={t("alertingSmsPlaceholder")}
|
||||
delimiterList={[",", "Enter"]}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function WebhookActionFields({
|
||||
index,
|
||||
control,
|
||||
@@ -663,160 +684,6 @@ function WebhookHeadersField({
|
||||
);
|
||||
}
|
||||
|
||||
function UserMultiSelect({
|
||||
orgId,
|
||||
value,
|
||||
onChange
|
||||
}: {
|
||||
orgId: string;
|
||||
value: string[];
|
||||
onChange: (v: string[]) => void;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [q, setQ] = useState("");
|
||||
const [debounced] = useDebounce(q, 150);
|
||||
const { data: users = [] } = useQuery(orgQueries.users({ orgId }));
|
||||
const shown = useMemo(() => {
|
||||
const qq = debounced.trim().toLowerCase();
|
||||
if (!qq) return users.slice(0, 200);
|
||||
return users
|
||||
.filter((u) => {
|
||||
const label = getUserDisplayName({
|
||||
email: u.email,
|
||||
name: u.name,
|
||||
username: u.username
|
||||
}).toLowerCase();
|
||||
return (
|
||||
label.includes(qq) ||
|
||||
(u.email ?? "").toLowerCase().includes(qq)
|
||||
);
|
||||
})
|
||||
.slice(0, 200);
|
||||
}, [users, debounced]);
|
||||
const toggle = (id: string) => {
|
||||
if (value.includes(id)) {
|
||||
onChange(value.filter((x) => x !== id));
|
||||
} else {
|
||||
onChange([...value, id]);
|
||||
}
|
||||
};
|
||||
const summary =
|
||||
value.length === 0
|
||||
? t("alertingSelectUsers")
|
||||
: t("alertingUsersSelected", { count: value.length });
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
<span className="truncate">{summary}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder={t("searchPlaceholder")}
|
||||
value={q}
|
||||
onValueChange={setQ}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>{t("noResults")}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{shown.map((u) => {
|
||||
const uid = String(u.id);
|
||||
return (
|
||||
<CommandItem
|
||||
key={uid}
|
||||
value={uid}
|
||||
onSelect={() => toggle(uid)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
checked={value.includes(uid)}
|
||||
className="mr-2 pointer-events-none"
|
||||
/>
|
||||
{getUserDisplayName({
|
||||
email: u.email,
|
||||
name: u.name,
|
||||
username: u.username
|
||||
})}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function RoleMultiSelect({
|
||||
orgId,
|
||||
value,
|
||||
onChange
|
||||
}: {
|
||||
orgId: string;
|
||||
value: number[];
|
||||
onChange: (v: number[]) => void;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data: roles = [] } = useQuery(orgQueries.roles({ orgId }));
|
||||
const toggle = (id: number) => {
|
||||
if (value.includes(id)) {
|
||||
onChange(value.filter((x) => x !== id));
|
||||
} else {
|
||||
onChange([...value, id]);
|
||||
}
|
||||
};
|
||||
const summary =
|
||||
value.length === 0
|
||||
? t("alertingSelectRoles")
|
||||
: t("alertingRolesSelected", { count: value.length });
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
<span className="truncate">{summary}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
{roles.map((r) => (
|
||||
<CommandItem
|
||||
key={r.roleId}
|
||||
value={`role-${r.roleId}`}
|
||||
onSelect={() => toggle(r.roleId)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
checked={value.includes(r.roleId)}
|
||||
className="mr-2 pointer-events-none"
|
||||
/>
|
||||
{r.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export function AlertRuleSourceFields({
|
||||
orgId,
|
||||
control
|
||||
@@ -838,7 +705,8 @@ export function AlertRuleSourceFields({
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={(v) => {
|
||||
const next = v as AlertRuleFormValues["sourceType"];
|
||||
const next =
|
||||
v as AlertRuleFormValues["sourceType"];
|
||||
field.onChange(next);
|
||||
const curTrigger = getValues("trigger");
|
||||
if (next === "site") {
|
||||
@@ -970,4 +838,4 @@ export function AlertRuleTriggerFields({
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -104,14 +104,12 @@ function summarizeTrigger(v: AlertRuleFormValues, t: AlertRuleT) {
|
||||
function oneActionConfigured(a: AlertRuleFormAction): boolean {
|
||||
if (a.type === "notify") {
|
||||
return (
|
||||
a.userIds.length > 0 ||
|
||||
a.roleIds.length > 0 ||
|
||||
a.userTags.length > 0 ||
|
||||
a.roleTags.length > 0 ||
|
||||
a.emailTags.length > 0
|
||||
);
|
||||
}
|
||||
if (a.type === "sms") {
|
||||
return a.phoneTags.length > 0;
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(a.url.trim());
|
||||
return true;
|
||||
@@ -124,8 +122,6 @@ function actionTypeLabel(a: AlertRuleFormAction, t: AlertRuleT): string {
|
||||
switch (a.type) {
|
||||
case "notify":
|
||||
return t("alertingActionNotify");
|
||||
case "sms":
|
||||
return t("alertingActionSms");
|
||||
case "webhook":
|
||||
return t("alertingActionWebhook");
|
||||
}
|
||||
@@ -134,18 +130,18 @@ function actionTypeLabel(a: AlertRuleFormAction, t: AlertRuleT): string {
|
||||
function summarizeOneAction(a: AlertRuleFormAction, t: AlertRuleT): string {
|
||||
if (a.type === "notify") {
|
||||
if (
|
||||
a.userIds.length === 0 &&
|
||||
a.roleIds.length === 0 &&
|
||||
a.userTags.length === 0 &&
|
||||
a.roleTags.length === 0 &&
|
||||
a.emailTags.length === 0
|
||||
) {
|
||||
return t("alertingNodeNotConfigured");
|
||||
}
|
||||
const parts: string[] = [];
|
||||
if (a.userIds.length > 0) {
|
||||
parts.push(t("alertingUsersSelected", { count: a.userIds.length }));
|
||||
if (a.userTags.length > 0) {
|
||||
parts.push(t("alertingUsersSelected", { count: a.userTags.length }));
|
||||
}
|
||||
if (a.roleIds.length > 0) {
|
||||
parts.push(t("alertingRolesSelected", { count: a.roleIds.length }));
|
||||
if (a.roleTags.length > 0) {
|
||||
parts.push(t("alertingRolesSelected", { count: a.roleTags.length }));
|
||||
}
|
||||
if (a.emailTags.length > 0) {
|
||||
parts.push(
|
||||
@@ -154,12 +150,6 @@ function summarizeOneAction(a: AlertRuleFormAction, t: AlertRuleT): string {
|
||||
}
|
||||
return parts.join(" · ");
|
||||
}
|
||||
if (a.type === "sms") {
|
||||
if (a.phoneTags.length === 0) {
|
||||
return t("alertingNodeNotConfigured");
|
||||
}
|
||||
return `${t("alertingSmsNumbers")}: ${a.phoneTags.length}`;
|
||||
}
|
||||
const url = a.url.trim();
|
||||
if (!url) {
|
||||
return t("alertingNodeNotConfigured");
|
||||
@@ -676,23 +666,16 @@ export default function AlertRuleGraphEditor({
|
||||
)}
|
||||
</span>
|
||||
<DropdownAddAction
|
||||
onPick={(type) => {
|
||||
onAdd={(type) => {
|
||||
const newIndex =
|
||||
fields.length;
|
||||
if (type === "notify") {
|
||||
append({
|
||||
type: "notify",
|
||||
userIds: [],
|
||||
roleIds: [],
|
||||
userTags: [],
|
||||
roleTags: [],
|
||||
emailTags: []
|
||||
});
|
||||
} else if (
|
||||
type === "sms"
|
||||
) {
|
||||
append({
|
||||
type: "sms",
|
||||
phoneTags: []
|
||||
});
|
||||
} else {
|
||||
append({
|
||||
type: "webhook",
|
||||
|
||||
@@ -25,11 +25,10 @@ export type AlertTrigger =
|
||||
export type AlertRuleFormAction =
|
||||
| {
|
||||
type: "notify";
|
||||
userIds: string[];
|
||||
roleIds: number[];
|
||||
userTags: Tag[];
|
||||
roleTags: Tag[];
|
||||
emailTags: Tag[];
|
||||
}
|
||||
| { type: "sms"; phoneTags: Tag[] }
|
||||
| {
|
||||
type: "webhook";
|
||||
url: string;
|
||||
@@ -142,14 +141,10 @@ export function buildFormSchema(t: (k: string) => string) {
|
||||
z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("notify"),
|
||||
userIds: z.array(z.string()),
|
||||
roleIds: z.array(z.number()),
|
||||
userTags: z.array(tagSchema),
|
||||
roleTags: z.array(tagSchema),
|
||||
emailTags: z.array(tagSchema)
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("sms"),
|
||||
phoneTags: z.array(tagSchema)
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("webhook"),
|
||||
url: z.string(),
|
||||
@@ -218,24 +213,17 @@ export function buildFormSchema(t: (k: string) => string) {
|
||||
val.actions.forEach((a, i) => {
|
||||
if (a.type === "notify") {
|
||||
if (
|
||||
a.userIds.length === 0 &&
|
||||
a.roleIds.length === 0 &&
|
||||
a.userTags.length === 0 &&
|
||||
a.roleTags.length === 0 &&
|
||||
a.emailTags.length === 0
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t("alertingErrorNotifyRecipients"),
|
||||
path: ["actions", i, "userIds"]
|
||||
path: ["actions", i, "userTags"]
|
||||
});
|
||||
}
|
||||
}
|
||||
if (a.type === "sms" && a.phoneTags.length === 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t("alertingErrorSmsPhones"),
|
||||
path: ["actions", i, "phoneTags"]
|
||||
});
|
||||
}
|
||||
if (a.type === "webhook") {
|
||||
try {
|
||||
new URL(a.url.trim());
|
||||
@@ -266,8 +254,8 @@ export function defaultFormValues(): AlertRuleFormValues {
|
||||
actions: [
|
||||
{
|
||||
type: "notify",
|
||||
userIds: [],
|
||||
roleIds: [],
|
||||
userTags: [],
|
||||
roleTags: [],
|
||||
emailTags: []
|
||||
}
|
||||
]
|
||||
@@ -287,21 +275,20 @@ export function apiResponseToFormValues(
|
||||
: "health_check";
|
||||
|
||||
// Collect notify recipients into a single notify action (if any)
|
||||
const userIds = rule.recipients
|
||||
const userTags = rule.recipients
|
||||
.filter((r) => r.userId != null)
|
||||
.map((r) => r.userId!);
|
||||
const roleIds = rule.recipients
|
||||
.map((r) => ({ id: r.userId!, text: r.userId! }));
|
||||
const roleTags = rule.recipients
|
||||
.filter((r) => r.roleId != null)
|
||||
.map((r) => parseInt(r.roleId!, 10))
|
||||
.filter((n) => !isNaN(n));
|
||||
.map((r) => ({ id: r.roleId!, text: r.roleId! }));
|
||||
const emailTags = rule.recipients
|
||||
.filter((r) => r.email != null)
|
||||
.map((r) => ({ id: r.email!, text: r.email! }));
|
||||
|
||||
const actions: AlertRuleFormAction[] = [];
|
||||
|
||||
if (userIds.length > 0 || roleIds.length > 0 || emailTags.length > 0) {
|
||||
actions.push({ type: "notify", userIds, roleIds, emailTags });
|
||||
if (userTags.length > 0 || roleTags.length > 0 || emailTags.length > 0) {
|
||||
actions.push({ type: "notify", userTags, roleTags, emailTags });
|
||||
}
|
||||
|
||||
// Each webhook action becomes its own form webhook action
|
||||
@@ -319,8 +306,8 @@ export function apiResponseToFormValues(
|
||||
if (actions.length === 0) {
|
||||
actions.push({
|
||||
type: "notify",
|
||||
userIds: [],
|
||||
roleIds: [],
|
||||
userTags: [],
|
||||
roleTags: [],
|
||||
emailTags: []
|
||||
});
|
||||
}
|
||||
@@ -354,8 +341,8 @@ export function formValuesToApiPayload(
|
||||
|
||||
for (const action of values.actions) {
|
||||
if (action.type === "notify") {
|
||||
allUserIds.push(...action.userIds);
|
||||
allRoleIds.push(...action.roleIds.map(String));
|
||||
allUserIds.push(...action.userTags.map((t) => t.id));
|
||||
allRoleIds.push(...action.roleTags.map((t) => t.id));
|
||||
allEmails.push(
|
||||
...action.emailTags
|
||||
.map((t) => t.text.trim())
|
||||
@@ -379,7 +366,6 @@ export function formValuesToApiPayload(
|
||||
: {})
|
||||
});
|
||||
}
|
||||
// sms is not supported by the backend; silently skip
|
||||
}
|
||||
|
||||
// Deduplicate
|
||||
@@ -398,4 +384,4 @@ export function formValuesToApiPayload(
|
||||
emails: uniqueEmails,
|
||||
webhookActions
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,6 @@ export const alertActionSchema = z.discriminatedUnion("type", [
|
||||
roleIds: z.array(z.number()),
|
||||
emails: z.array(z.string())
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("sms"),
|
||||
phoneNumbers: z.array(z.string())
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("webhook"),
|
||||
url: z.string().url(),
|
||||
|
||||
Reference in New Issue
Block a user