mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-25 10:12:35 +00:00
Trying to use more consistant components
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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")}
|
||||||
</Button>
|
</CommandItem>
|
||||||
<Button
|
<CommandItem
|
||||||
type="button"
|
onSelect={() => {
|
||||||
variant="ghost"
|
onAdd("webhook");
|
||||||
className="justify-start"
|
|
||||||
onClick={() => {
|
|
||||||
onPick("sms");
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("alertingActionSms")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
className="justify-start"
|
|
||||||
onClick={() => {
|
|
||||||
onPick("webhook");
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("alertingActionWebhook")}
|
{t("alertingActionWebhook")}
|
||||||
</Button>
|
</CommandItem>
|
||||||
</div>
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
</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
|
||||||
|
control={control}
|
||||||
|
name={`actions.${index}.userTags`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>{t("alertingNotifyUsers")}</FormLabel>
|
<FormLabel>{t("alertingNotifyUsers")}</FormLabel>
|
||||||
<UserMultiSelect
|
<FormControl>
|
||||||
orgId={orgId}
|
<TagInput
|
||||||
value={userIds}
|
{...field}
|
||||||
onChange={(ids) =>
|
activeTagIndex={activeUsersTagIndex}
|
||||||
form.setValue(`actions.${index}.userIds`, ids)
|
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>
|
</FormItem>
|
||||||
<FormItem>
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name={`actions.${index}.roleTags`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>{t("alertingNotifyRoles")}</FormLabel>
|
<FormLabel>{t("alertingNotifyRoles")}</FormLabel>
|
||||||
<RoleMultiSelect
|
<FormControl>
|
||||||
orgId={orgId}
|
<TagInput
|
||||||
value={roleIds}
|
{...field}
|
||||||
onChange={(ids) =>
|
activeTagIndex={activeRolesTagIndex}
|
||||||
form.setValue(`actions.${index}.roleIds`, ids)
|
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>
|
</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") {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user