Adding external actions

This commit is contained in:
Owen
2026-04-21 11:52:15 -07:00
parent ed327626bb
commit 206b3a7d22
6 changed files with 192 additions and 76 deletions

View File

@@ -1408,7 +1408,10 @@
"alertingSectionActions": "Actions", "alertingSectionActions": "Actions",
"alertingAddAction": "Add action", "alertingAddAction": "Add action",
"alertingActionNotify": "Email", "alertingActionNotify": "Email",
"alertingActionNotifyDescription": "Send email notifications to users or roles",
"alertingActionWebhook": "Webhook", "alertingActionWebhook": "Webhook",
"alertingActionWebhookDescription": "Send an HTTP request to a custom endpoint",
"alertingExternalIntegration": "External Integration",
"alertingActionType": "Action type", "alertingActionType": "Action type",
"alertingNotifyUsers": "Users", "alertingNotifyUsers": "Users",
"alertingNotifyRoles": "Roles", "alertingNotifyRoles": "Roles",

BIN
public/third-party/incidentio.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/third-party/opsgenie.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
public/third-party/pgd.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
public/third-party/servicenow.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -44,13 +44,39 @@ import {
} from "@app/lib/alertRuleForm"; } from "@app/lib/alertRuleForm";
import { orgQueries } from "@app/lib/queries"; import { orgQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { ChevronsUpDown, Plus, Trash2 } from "lucide-react"; import { ContactSalesBanner } from "@app/components/ContactSalesBanner";
import { Bell, Globe, ChevronsUpDown, Plus, Trash2 } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import type { Control, UseFormReturn } from "react-hook-form"; import type { Control, UseFormReturn } from "react-hook-form";
import { useFormContext, useWatch } from "react-hook-form"; import { useFormContext, useWatch } from "react-hook-form";
import { useDebounce } from "use-debounce"; import { useDebounce } from "use-debounce";
const EXTERNAL_INTEGRATIONS = [
{
id: "pagerduty",
name: "PagerDuty",
logo: "/third-party/pgd.png"
},
{
id: "opsgenie",
name: "Opsgenie",
logo: "/third-party/opsgenie.png"
},
{
id: "servicenow",
name: "ServiceNow",
logo: "/third-party/servicenow.png"
},
{
id: "incidentio",
name: "Incident.io",
logo: "/third-party/incidentio.png"
}
] as const;
const EXTERNAL_IDS = EXTERNAL_INTEGRATIONS.map((i) => i.id);
export function DropdownAddAction({ export function DropdownAddAction({
onAdd onAdd
}: { }: {
@@ -58,8 +84,15 @@ export function DropdownAddAction({
}) { }) {
const t = useTranslations(); const t = useTranslations();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [salesFor, setSalesFor] = useState<string | null>(null);
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover
open={open}
onOpenChange={(o) => {
setOpen(o);
if (!o) setSalesFor(null);
}}
>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<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" />
@@ -67,29 +100,84 @@ export function DropdownAddAction({
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0 w-48" align="start"> <PopoverContent
<Command> className={salesFor ? "w-80 p-3" : "p-0 w-52"}
<CommandList> align="start"
<CommandGroup> >
<CommandItem {salesFor ? (
onSelect={() => { <div className="space-y-3">
onAdd("notify"); <div className="flex items-center gap-2">
setOpen(false); <img
}} src={
> EXTERNAL_INTEGRATIONS.find(
{t("alertingActionNotify")} (i) => i.id === salesFor
</CommandItem> )?.logo
<CommandItem }
onSelect={() => { alt={salesFor}
onAdd("webhook"); className="h-5 w-5 object-contain"
setOpen(false); />
}} <span className="text-sm font-medium">
> {
{t("alertingActionWebhook")} EXTERNAL_INTEGRATIONS.find(
</CommandItem> (i) => i.id === salesFor
</CommandGroup> )?.name
</CommandList> }
</Command> </span>
</div>
<ContactSalesBanner />
<Button
type="button"
variant="ghost"
size="sm"
className="w-full"
onClick={() => setSalesFor(null)}
>
Back
</Button>
</div>
) : (
<Command>
<CommandList>
<CommandGroup heading="Built-in">
<CommandItem
onSelect={() => {
onAdd("notify");
setOpen(false);
}}
>
<Bell className="h-4 w-4 mr-2 text-muted-foreground" />
{t("alertingActionNotify")}
</CommandItem>
<CommandItem
onSelect={() => {
onAdd("webhook");
setOpen(false);
}}
>
<Globe className="h-4 w-4 mr-2 text-muted-foreground" />
{t("alertingActionWebhook")}
</CommandItem>
</CommandGroup>
<CommandGroup heading="Integrations">
{EXTERNAL_INTEGRATIONS.map((integration) => (
<CommandItem
key={integration.id}
onSelect={() =>
setSalesFor(integration.id)
}
>
<img
src={integration.logo}
alt={integration.name}
className="h-4 w-4 mr-2 object-contain"
/>
{integration.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
)}
</PopoverContent> </PopoverContent>
</Popover> </Popover>
); );
@@ -382,6 +470,43 @@ export function ActionBlock({
}) { }) {
const t = useTranslations(); const t = useTranslations();
const type = useWatch({ control, name: `actions.${index}.type` }); const type = useWatch({ control, name: `actions.${index}.type` });
const [displayType, setDisplayType] = useState<string>(type ?? "notify");
useEffect(() => {
if (!EXTERNAL_IDS.includes(displayType as any)) {
setDisplayType(type ?? "notify");
}
}, [type]);
const isPremium = EXTERNAL_IDS.includes(displayType as any);
const actionTypeOptions = [
{
id: "notify",
title: t("alertingActionNotify"),
description: t("alertingActionNotifyDescription"),
icon: <Bell className="h-5 w-5" />
},
{
id: "webhook",
title: t("alertingActionWebhook"),
description: t("alertingActionWebhookDescription"),
icon: <Globe className="h-5 w-5" />
},
...EXTERNAL_INTEGRATIONS.map((integration) => ({
id: integration.id,
title: integration.name,
description: t("alertingExternalIntegration"),
icon: (
<img
src={integration.logo}
alt={integration.name}
className="h-5 w-5 object-contain"
/>
)
}))
];
return ( return (
<div className="rounded-lg border p-4 space-y-3 relative"> <div className="rounded-lg border p-4 space-y-3 relative">
{canRemove && ( {canRemove && (
@@ -395,56 +520,44 @@ export function ActionBlock({
<Trash2 className="h-4 w-4 text-destructive" /> <Trash2 className="h-4 w-4 text-destructive" />
</Button> </Button>
)} )}
<FormField <div className="space-y-2">
control={control} <Label className="text-sm font-medium">
name={`actions.${index}.type`} {t("alertingActionType")}
render={({ field }) => ( </Label>
<FormItem> <StrategySelect
<FormLabel>{t("alertingActionType")}</FormLabel> options={actionTypeOptions}
<Select value={displayType}
value={field.value} cols={2}
onValueChange={(v) => { onChange={(v) => {
const nt = v as AlertRuleFormAction["type"]; setDisplayType(v);
if (nt === "notify") { if (!EXTERNAL_IDS.includes(v as any)) {
onUpdate({ const nt = v as AlertRuleFormAction["type"];
type: "notify", if (nt === "notify") {
userTags: [], onUpdate({
roleTags: [], type: "notify",
emailTags: [] userTags: [],
}); roleTags: [],
} else { emailTags: []
onUpdate({ });
type: "webhook", } else {
url: "", onUpdate({
method: "POST", type: "webhook",
headers: [], url: "",
authType: "none", method: "POST",
bearerToken: "", headers: [],
basicCredentials: "", authType: "none",
customHeaderName: "", bearerToken: "",
customHeaderValue: "" basicCredentials: "",
}); customHeaderName: "",
} customHeaderValue: ""
}} });
> }
<FormControl> }
<SelectTrigger className="max-w-xs"> }}
<SelectValue /> />
</SelectTrigger> </div>
</FormControl> {isPremium && <ContactSalesBanner />}
<SelectContent> {!isPremium && type === "notify" && (
<SelectItem value="notify">
{t("alertingActionNotify")}
</SelectItem>
<SelectItem value="webhook">
{t("alertingActionWebhook")}
</SelectItem>
</SelectContent>
</Select>
</FormItem>
)}
/>
{type === "notify" && (
<NotifyActionFields <NotifyActionFields
orgId={orgId} orgId={orgId}
index={index} index={index}
@@ -452,7 +565,7 @@ export function ActionBlock({
form={form} form={form}
/> />
)} )}
{type === "webhook" && ( {!isPremium && type === "webhook" && (
<WebhookActionFields <WebhookActionFields
index={index} index={index}
control={control} control={control}