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,16 +100,52 @@ 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
className={salesFor ? "w-80 p-3" : "p-0 w-52"}
align="start"
>
{salesFor ? (
<div className="space-y-3">
<div className="flex items-center gap-2">
<img
src={
EXTERNAL_INTEGRATIONS.find(
(i) => i.id === salesFor
)?.logo
}
alt={salesFor}
className="h-5 w-5 object-contain"
/>
<span className="text-sm font-medium">
{
EXTERNAL_INTEGRATIONS.find(
(i) => i.id === salesFor
)?.name
}
</span>
</div>
<ContactSalesBanner />
<Button
type="button"
variant="ghost"
size="sm"
className="w-full"
onClick={() => setSalesFor(null)}
>
Back
</Button>
</div>
) : (
<Command> <Command>
<CommandList> <CommandList>
<CommandGroup> <CommandGroup heading="Built-in">
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
onAdd("notify"); onAdd("notify");
setOpen(false); setOpen(false);
}} }}
> >
<Bell className="h-4 w-4 mr-2 text-muted-foreground" />
{t("alertingActionNotify")} {t("alertingActionNotify")}
</CommandItem> </CommandItem>
<CommandItem <CommandItem
@@ -85,11 +154,30 @@ export function DropdownAddAction({
setOpen(false); setOpen(false);
}} }}
> >
<Globe className="h-4 w-4 mr-2 text-muted-foreground" />
{t("alertingActionWebhook")} {t("alertingActionWebhook")}
</CommandItem> </CommandItem>
</CommandGroup> </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> </CommandList>
</Command> </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,15 +520,17 @@ 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) => {
setDisplayType(v);
if (!EXTERNAL_IDS.includes(v as any)) {
const nt = v as AlertRuleFormAction["type"]; const nt = v as AlertRuleFormAction["type"];
if (nt === "notify") { if (nt === "notify") {
onUpdate({ onUpdate({
@@ -425,26 +552,12 @@ export function ActionBlock({
customHeaderValue: "" customHeaderValue: ""
}); });
} }
}
}} }}
>
<FormControl>
<SelectTrigger className="max-w-xs">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="notify">
{t("alertingActionNotify")}
</SelectItem>
<SelectItem value="webhook">
{t("alertingActionWebhook")}
</SelectItem>
</SelectContent>
</Select>
</FormItem>
)}
/> />
{type === "notify" && ( </div>
{isPremium && <ContactSalesBanner />}
{!isPremium && 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}