mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-27 03:02:30 +00:00
Adding external actions
This commit is contained in:
@@ -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
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
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
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
BIN
public/third-party/servicenow.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user