Trying to use more consistant components

This commit is contained in:
Owen
2026-04-15 15:51:41 -07:00
parent 5e505224d0
commit 55595ec042
5 changed files with 180 additions and 352 deletions

View File

@@ -1382,16 +1382,13 @@
"alertingTriggerHcUnhealthy": "Health check unhealthy", "alertingTriggerHcUnhealthy": "Health check unhealthy",
"alertingSectionActions": "Actions", "alertingSectionActions": "Actions",
"alertingAddAction": "Add action", "alertingAddAction": "Add action",
"alertingActionNotify": "Notify", "alertingActionNotify": "Email",
"alertingActionSms": "SMS",
"alertingActionWebhook": "Webhook", "alertingActionWebhook": "Webhook",
"alertingActionType": "Action type", "alertingActionType": "Action type",
"alertingNotifyUsers": "Users", "alertingNotifyUsers": "Users",
"alertingNotifyRoles": "Roles", "alertingNotifyRoles": "Roles",
"alertingNotifyEmails": "Email addresses", "alertingNotifyEmails": "Email addresses",
"alertingEmailPlaceholder": "Add email and press Enter", "alertingEmailPlaceholder": "Add email and press Enter",
"alertingSmsNumbers": "Phone numbers",
"alertingSmsPlaceholder": "Add number and press Enter",
"alertingWebhookMethod": "HTTP method", "alertingWebhookMethod": "HTTP method",
"alertingWebhookSecret": "Signing secret (optional)", "alertingWebhookSecret": "Signing secret (optional)",
"alertingWebhookSecretPlaceholder": "HMAC secret", "alertingWebhookSecretPlaceholder": "HMAC secret",
@@ -1416,8 +1413,6 @@
"alertingErrorTriggerSite": "Choose a site trigger", "alertingErrorTriggerSite": "Choose a site trigger",
"alertingErrorTriggerHealth": "Choose a health check trigger", "alertingErrorTriggerHealth": "Choose a health check trigger",
"alertingErrorNotifyRecipients": "Pick users, roles, or at least one email", "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", "alertingConfigureSource": "Configure Source",
"alertingConfigureTrigger": "Configure Trigger", "alertingConfigureTrigger": "Configure Trigger",
"alertingConfigureActions": "Configure Actions", "alertingConfigureActions": "Configure Actions",

View File

@@ -30,7 +30,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from "@app/components/ui/select"; } 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 { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { import {
type AlertRuleFormAction, type AlertRuleFormAction,
@@ -46,9 +46,9 @@ import { useFormContext, useWatch } from "react-hook-form";
import { useDebounce } from "use-debounce"; import { useDebounce } from "use-debounce";
export function DropdownAddAction({ export function DropdownAddAction({
onPick onAdd
}: { }: {
onPick: (type: "notify" | "sms" | "webhook") => void; onAdd: (type: AlertRuleFormAction["type"]) => void;
}) { }) {
const t = useTranslations(); const t = useTranslations();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -58,44 +58,32 @@ export function DropdownAddAction({
<Button type="button" variant="outline" size="sm"> <Button type="button" variant="outline" size="sm">
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
{t("alertingAddAction")} {t("alertingAddAction")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-52 p-2" align="end"> <PopoverContent className="p-0 w-48" align="start">
<div className="flex flex-col gap-1"> <Command>
<Button <CommandList>
type="button" <CommandGroup>
variant="ghost" <CommandItem
className="justify-start" onSelect={() => {
onClick={() => { onAdd("notify");
onPick("notify"); setOpen(false);
setOpen(false); }}
}} >
> {t("alertingActionNotify")}
{t("alertingActionNotify")} </CommandItem>
</Button> <CommandItem
<Button onSelect={() => {
type="button" onAdd("webhook");
variant="ghost" setOpen(false);
className="justify-start" }}
onClick={() => { >
onPick("sms"); {t("alertingActionWebhook")}
setOpen(false); </CommandItem>
}} </CommandGroup>
> </CommandList>
{t("alertingActionSms")} </Command>
</Button>
<Button
type="button"
variant="ghost"
className="justify-start"
onClick={() => {
onPick("webhook");
setOpen(false);
}}
>
{t("alertingActionWebhook")}
</Button>
</div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
); );
@@ -325,15 +313,10 @@ export function ActionBlock({
if (nt === "notify") { if (nt === "notify") {
form.setValue(`actions.${index}`, { form.setValue(`actions.${index}`, {
type: "notify", type: "notify",
userIds: [], userTags: [],
roleIds: [], roleTags: [],
emailTags: [] emailTags: []
}); });
} else if (nt === "sms") {
form.setValue(`actions.${index}`, {
type: "sms",
phoneTags: []
});
} else { } else {
form.setValue(`actions.${index}`, { form.setValue(`actions.${index}`, {
type: "webhook", type: "webhook",
@@ -354,9 +337,6 @@ export function ActionBlock({
<SelectItem value="notify"> <SelectItem value="notify">
{t("alertingActionNotify")} {t("alertingActionNotify")}
</SelectItem> </SelectItem>
<SelectItem value="sms">
{t("alertingActionSms")}
</SelectItem>
<SelectItem value="webhook"> <SelectItem value="webhook">
{t("alertingActionWebhook")} {t("alertingActionWebhook")}
</SelectItem> </SelectItem>
@@ -373,9 +353,6 @@ export function ActionBlock({
form={form} form={form}
/> />
)} )}
{type === "sms" && (
<SmsActionFields index={index} control={control} form={form} />
)}
{type === "webhook" && ( {type === "webhook" && (
<WebhookActionFields <WebhookActionFields
index={index} index={index}
@@ -399,41 +376,124 @@ function NotifyActionFields({
form: UseFormReturn<AlertRuleFormValues>; form: UseFormReturn<AlertRuleFormValues>;
}) { }) {
const t = useTranslations(); const t = useTranslations();
const [emailActiveIdx, setEmailActiveIdx] = useState<number | null>(null); const [emailActiveIdx, setEmailActiveIdx] = useState<number | null>(null);
const userIds = form.watch(`actions.${index}.userIds`) ?? []; const [activeUsersTagIndex, setActiveUsersTagIndex] = useState<
const roleIds = form.watch(`actions.${index}.roleIds`) ?? []; number | null
const emailTags = form.watch(`actions.${index}.emailTags`) ?? []; >(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 ( return (
<div className="space-y-3 pt-1"> <div className="space-y-3 pt-1">
<FormItem> <FormField
<FormLabel>{t("alertingNotifyUsers")}</FormLabel> control={control}
<UserMultiSelect name={`actions.${index}.userTags`}
orgId={orgId} render={({ field }) => (
value={userIds} <FormItem className="flex flex-col items-start">
onChange={(ids) => <FormLabel>{t("alertingNotifyUsers")}</FormLabel>
form.setValue(`actions.${index}.userIds`, ids) <FormControl>
} <TagInput
/> {...field}
</FormItem> activeTagIndex={activeUsersTagIndex}
<FormItem> setActiveTagIndex={setActiveUsersTagIndex}
<FormLabel>{t("alertingNotifyRoles")}</FormLabel> placeholder={t("alertingSelectUsers")}
<RoleMultiSelect size="sm"
orgId={orgId} tags={userTags}
value={roleIds} setTags={(newTags) => {
onChange={(ids) => const next =
form.setValue(`actions.${index}.roleIds`, ids) typeof newTags === "function"
} ? newTags(userTags)
/> : newTags;
</FormItem> 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 <FormField
control={control} control={control}
name={`actions.${index}.emailTags`} name={`actions.${index}.emailTags`}
render={() => ( render={({ field }) => (
<FormItem> <FormItem className="flex flex-col items-start">
<FormLabel>{t("alertingNotifyEmails")}</FormLabel> <FormLabel>{t("alertingNotifyEmails")}</FormLabel>
<FormControl> <FormControl>
<TagInput <TagInput
{...field}
tags={emailTags} tags={emailTags}
setTags={(updater) => { setTags={(updater) => {
const next = const next =
@@ -442,12 +502,18 @@ function NotifyActionFields({
: updater; : updater;
form.setValue( form.setValue(
`actions.${index}.emailTags`, `actions.${index}.emailTags`,
next next as Tag[]
); );
}} }}
activeTagIndex={emailActiveIdx} activeTagIndex={emailActiveIdx}
setActiveTagIndex={setEmailActiveIdx} setActiveTagIndex={setEmailActiveIdx}
placeholder={t("alertingEmailPlaceholder")} placeholder={t("alertingEmailPlaceholder")}
size="sm"
allowDuplicates={false}
sortTags={true}
validateTag={(tag) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(tag)
}
delimiterList={[",", "Enter"]} delimiterList={[",", "Enter"]}
/> />
</FormControl> </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({ function WebhookActionFields({
index, index,
control, 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({ export function AlertRuleSourceFields({
orgId, orgId,
control control
@@ -838,7 +705,8 @@ export function AlertRuleSourceFields({
<Select <Select
value={field.value} value={field.value}
onValueChange={(v) => { onValueChange={(v) => {
const next = v as AlertRuleFormValues["sourceType"]; const next =
v as AlertRuleFormValues["sourceType"];
field.onChange(next); field.onChange(next);
const curTrigger = getValues("trigger"); const curTrigger = getValues("trigger");
if (next === "site") { if (next === "site") {
@@ -970,4 +838,4 @@ export function AlertRuleTriggerFields({
)} )}
/> />
); );
} }

View File

@@ -104,14 +104,12 @@ function summarizeTrigger(v: AlertRuleFormValues, t: AlertRuleT) {
function oneActionConfigured(a: AlertRuleFormAction): boolean { function oneActionConfigured(a: AlertRuleFormAction): boolean {
if (a.type === "notify") { if (a.type === "notify") {
return ( return (
a.userIds.length > 0 || a.userTags.length > 0 ||
a.roleIds.length > 0 || a.roleTags.length > 0 ||
a.emailTags.length > 0 a.emailTags.length > 0
); );
} }
if (a.type === "sms") {
return a.phoneTags.length > 0;
}
try { try {
new URL(a.url.trim()); new URL(a.url.trim());
return true; return true;
@@ -124,8 +122,6 @@ function actionTypeLabel(a: AlertRuleFormAction, t: AlertRuleT): string {
switch (a.type) { switch (a.type) {
case "notify": case "notify":
return t("alertingActionNotify"); return t("alertingActionNotify");
case "sms":
return t("alertingActionSms");
case "webhook": case "webhook":
return t("alertingActionWebhook"); return t("alertingActionWebhook");
} }
@@ -134,18 +130,18 @@ function actionTypeLabel(a: AlertRuleFormAction, t: AlertRuleT): string {
function summarizeOneAction(a: AlertRuleFormAction, t: AlertRuleT): string { function summarizeOneAction(a: AlertRuleFormAction, t: AlertRuleT): string {
if (a.type === "notify") { if (a.type === "notify") {
if ( if (
a.userIds.length === 0 && a.userTags.length === 0 &&
a.roleIds.length === 0 && a.roleTags.length === 0 &&
a.emailTags.length === 0 a.emailTags.length === 0
) { ) {
return t("alertingNodeNotConfigured"); return t("alertingNodeNotConfigured");
} }
const parts: string[] = []; const parts: string[] = [];
if (a.userIds.length > 0) { if (a.userTags.length > 0) {
parts.push(t("alertingUsersSelected", { count: a.userIds.length })); parts.push(t("alertingUsersSelected", { count: a.userTags.length }));
} }
if (a.roleIds.length > 0) { if (a.roleTags.length > 0) {
parts.push(t("alertingRolesSelected", { count: a.roleIds.length })); parts.push(t("alertingRolesSelected", { count: a.roleTags.length }));
} }
if (a.emailTags.length > 0) { if (a.emailTags.length > 0) {
parts.push( parts.push(
@@ -154,12 +150,6 @@ function summarizeOneAction(a: AlertRuleFormAction, t: AlertRuleT): string {
} }
return parts.join(" · "); 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(); const url = a.url.trim();
if (!url) { if (!url) {
return t("alertingNodeNotConfigured"); return t("alertingNodeNotConfigured");
@@ -676,23 +666,16 @@ export default function AlertRuleGraphEditor({
)} )}
</span> </span>
<DropdownAddAction <DropdownAddAction
onPick={(type) => { onAdd={(type) => {
const newIndex = const newIndex =
fields.length; fields.length;
if (type === "notify") { if (type === "notify") {
append({ append({
type: "notify", type: "notify",
userIds: [], userTags: [],
roleIds: [], roleTags: [],
emailTags: [] emailTags: []
}); });
} else if (
type === "sms"
) {
append({
type: "sms",
phoneTags: []
});
} else { } else {
append({ append({
type: "webhook", type: "webhook",

View File

@@ -25,11 +25,10 @@ export type AlertTrigger =
export type AlertRuleFormAction = export type AlertRuleFormAction =
| { | {
type: "notify"; type: "notify";
userIds: string[]; userTags: Tag[];
roleIds: number[]; roleTags: Tag[];
emailTags: Tag[]; emailTags: Tag[];
} }
| { type: "sms"; phoneTags: Tag[] }
| { | {
type: "webhook"; type: "webhook";
url: string; url: string;
@@ -142,14 +141,10 @@ export function buildFormSchema(t: (k: string) => string) {
z.discriminatedUnion("type", [ z.discriminatedUnion("type", [
z.object({ z.object({
type: z.literal("notify"), type: z.literal("notify"),
userIds: z.array(z.string()), userTags: z.array(tagSchema),
roleIds: z.array(z.number()), roleTags: z.array(tagSchema),
emailTags: z.array(tagSchema) emailTags: z.array(tagSchema)
}), }),
z.object({
type: z.literal("sms"),
phoneTags: z.array(tagSchema)
}),
z.object({ z.object({
type: z.literal("webhook"), type: z.literal("webhook"),
url: z.string(), url: z.string(),
@@ -218,24 +213,17 @@ export function buildFormSchema(t: (k: string) => string) {
val.actions.forEach((a, i) => { val.actions.forEach((a, i) => {
if (a.type === "notify") { if (a.type === "notify") {
if ( if (
a.userIds.length === 0 && a.userTags.length === 0 &&
a.roleIds.length === 0 && a.roleTags.length === 0 &&
a.emailTags.length === 0 a.emailTags.length === 0
) { ) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: t("alertingErrorNotifyRecipients"), 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") { if (a.type === "webhook") {
try { try {
new URL(a.url.trim()); new URL(a.url.trim());
@@ -266,8 +254,8 @@ export function defaultFormValues(): AlertRuleFormValues {
actions: [ actions: [
{ {
type: "notify", type: "notify",
userIds: [], userTags: [],
roleIds: [], roleTags: [],
emailTags: [] emailTags: []
} }
] ]
@@ -287,21 +275,20 @@ export function apiResponseToFormValues(
: "health_check"; : "health_check";
// Collect notify recipients into a single notify action (if any) // Collect notify recipients into a single notify action (if any)
const userIds = rule.recipients const userTags = rule.recipients
.filter((r) => r.userId != null) .filter((r) => r.userId != null)
.map((r) => r.userId!); .map((r) => ({ id: r.userId!, text: r.userId! }));
const roleIds = rule.recipients const roleTags = rule.recipients
.filter((r) => r.roleId != null) .filter((r) => r.roleId != null)
.map((r) => parseInt(r.roleId!, 10)) .map((r) => ({ id: r.roleId!, text: r.roleId! }));
.filter((n) => !isNaN(n));
const emailTags = rule.recipients const emailTags = rule.recipients
.filter((r) => r.email != null) .filter((r) => r.email != null)
.map((r) => ({ id: r.email!, text: r.email! })); .map((r) => ({ id: r.email!, text: r.email! }));
const actions: AlertRuleFormAction[] = []; const actions: AlertRuleFormAction[] = [];
if (userIds.length > 0 || roleIds.length > 0 || emailTags.length > 0) { if (userTags.length > 0 || roleTags.length > 0 || emailTags.length > 0) {
actions.push({ type: "notify", userIds, roleIds, emailTags }); actions.push({ type: "notify", userTags, roleTags, emailTags });
} }
// Each webhook action becomes its own form webhook action // Each webhook action becomes its own form webhook action
@@ -319,8 +306,8 @@ export function apiResponseToFormValues(
if (actions.length === 0) { if (actions.length === 0) {
actions.push({ actions.push({
type: "notify", type: "notify",
userIds: [], userTags: [],
roleIds: [], roleTags: [],
emailTags: [] emailTags: []
}); });
} }
@@ -354,8 +341,8 @@ export function formValuesToApiPayload(
for (const action of values.actions) { for (const action of values.actions) {
if (action.type === "notify") { if (action.type === "notify") {
allUserIds.push(...action.userIds); allUserIds.push(...action.userTags.map((t) => t.id));
allRoleIds.push(...action.roleIds.map(String)); allRoleIds.push(...action.roleTags.map((t) => t.id));
allEmails.push( allEmails.push(
...action.emailTags ...action.emailTags
.map((t) => t.text.trim()) .map((t) => t.text.trim())
@@ -379,7 +366,6 @@ export function formValuesToApiPayload(
: {}) : {})
}); });
} }
// sms is not supported by the backend; silently skip
} }
// Deduplicate // Deduplicate
@@ -398,4 +384,4 @@ export function formValuesToApiPayload(
emails: uniqueEmails, emails: uniqueEmails,
webhookActions webhookActions
}; };
} }

View File

@@ -14,10 +14,6 @@ export const alertActionSchema = z.discriminatedUnion("type", [
roleIds: z.array(z.number()), roleIds: z.array(z.number()),
emails: z.array(z.string()) emails: z.array(z.string())
}), }),
z.object({
type: z.literal("sms"),
phoneNumbers: z.array(z.string())
}),
z.object({ z.object({
type: z.literal("webhook"), type: z.literal("webhook"),
url: z.string().url(), url: z.string().url(),