diff --git a/keep-ui/app/(keep)/alerts/EnrichAlertSidePanel.tsx b/keep-ui/app/(keep)/alerts/EnrichAlertSidePanel.tsx new file mode 100644 index 000000000..0bf375d21 --- /dev/null +++ b/keep-ui/app/(keep)/alerts/EnrichAlertSidePanel.tsx @@ -0,0 +1,215 @@ +import {AlertDto} from "./models"; +import {Dialog, Transition} from "@headlessui/react"; +import React, {Fragment, useEffect, useState} from "react"; +import {Button, TextInput} from "@tremor/react"; +import {useSession} from "next-auth/react"; +import {useApiUrl} from "utils/hooks/useConfig"; +import {toast} from "react-toastify"; +import SidePanel from "@/components/SidePanel" + +interface EnrichAlertModalProps { + alert: AlertDto | null | undefined; + isOpen: boolean; + handleClose: () => void; + mutate: () => void; +} + +const EnrichAlertSidePanel: React.FC = ({ + alert, + isOpen, + handleClose, + mutate, + }) => { + const { data: session } = useSession(); + const apiUrl = useApiUrl(); + + const [customFields, setCustomFields] = useState< + { key: string; value: string }[] + >([]); + + const [preEnrichedFields, setPreEnrichedFields] = useState< + { key: string; value: string }[] + >([]); + + const [finalData, setFinalData] = useState>({}); + const [isDataValid, setIsDataValid] = useState(false); + + const addCustomField = () => { + setCustomFields((prev) => [...prev, { key: "", value: "" }]); + }; + + const updateCustomField = ( + index: number, + field: "key" | "value", + value: string, + ) => { + setCustomFields((prev) => + prev.map((item, i) => (i === index ? { ...item, [field]: value } : item)), + ); + }; + + const removeCustomField = (index: number) => { + setCustomFields((prev) => prev.filter((_, i) => i !== index)); + }; + + useEffect(() => { + const preEnrichedFields = + alert?.enriched_fields?.map((key) => { + return { key, value: alert[key as keyof AlertDto] as any }; + }) || []; + setCustomFields(preEnrichedFields); + setPreEnrichedFields(preEnrichedFields); + }, [alert]); + + useEffect(() => { + const validateData = () => { + const areFieldsIdentical = + customFields.length === preEnrichedFields.length && + customFields.every((field) => { + const matchingField = preEnrichedFields.find( + (preField) => preField.key === field.key, + ); + return matchingField && matchingField.value === field.value; + }); + + if (areFieldsIdentical) { + setIsDataValid(false); + return; + } + + const keys = customFields.map((field) => field.key); + const hasEmptyKeys = keys.some((key) => !key); + const hasDuplicateKeys = new Set(keys).size !== keys.length; + + setIsDataValid(!hasEmptyKeys && !hasDuplicateKeys); + }; + + const calculateFinalData = () => { + return customFields.reduce( + (acc, field) => { + if (field.key) { + acc[field.key] = field.value; + } + return acc; + }, + {} as Record, + ); + }; + setFinalData(calculateFinalData()); + validateData(); + }, [customFields, preEnrichedFields]); + + useEffect(() => { + if (!isOpen) { + setFinalData({}); + setIsDataValid(false); + } + }, [isOpen]); + + const handleSave = async () => { + const requestData = { + enrichments: finalData, + fingerprint: alert?.fingerprint, + }; + + const enrichedFieldKeys = customFields.map((field) => field.key); + const preEnrichedFieldKeys = preEnrichedFields.map((field) => field.key); + + const unEnrichedFields = preEnrichedFieldKeys.filter((key) => { + if (!enrichedFieldKeys.includes(key)) { + return key; + } + }); + + let fieldsUnEnrichedSuccessfully = true; + + if (unEnrichedFields.length != 0) { + const unEnrichmentResponse = await fetch(`${apiUrl}/alerts/unenrich`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${session?.accessToken}`, + }, + body: JSON.stringify({ + fingerprint: alert?.fingerprint, + enrichments: unEnrichedFields, + }), + }); + fieldsUnEnrichedSuccessfully = unEnrichmentResponse.ok; + } + + const response = await fetch(`${apiUrl}/alerts/enrich`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${session?.accessToken}`, + }, + body: JSON.stringify(requestData), + }); + + if (response.ok && fieldsUnEnrichedSuccessfully) { + toast.success("Alert enriched successfully"); + await mutate(); + handleClose(); + } else { + toast.error("Failed to enrich alert"); + } + }; + + const renderCustomFields = () => + customFields.map((field, index) => ( +
+ updateCustomField(index, "key", e.target.value)} + required + className="w-1/3" + /> + updateCustomField(index, "value", e.target.value)} + className="w-full" + /> + +
+ )); + + return ( + +
+

Enrich Alert

+
+ +
+ {renderCustomFields()} +
+ +
+ + + +
+
+ ); +}; + +export default EnrichAlertSidePanel; diff --git a/keep-ui/app/(keep)/alerts/alert-menu.tsx b/keep-ui/app/(keep)/alerts/alert-menu.tsx index 58846e5ea..60cea9491 100644 --- a/keep-ui/app/(keep)/alerts/alert-menu.tsx +++ b/keep-ui/app/(keep)/alerts/alert-menu.tsx @@ -9,6 +9,7 @@ import { UserPlusIcon, PlayIcon, EyeIcon, + AdjustmentsHorizontalIcon, } from "@heroicons/react/24/outline"; import { IoNotificationsOffOutline } from "react-icons/io5"; @@ -216,6 +217,24 @@ export default function AlertMenu({ )} + + {({ active }) => ( + + )} + {canAssign && ( {({ active }) => ( diff --git a/keep-ui/app/(keep)/alerts/alerts.tsx b/keep-ui/app/(keep)/alerts/alerts.tsx index b5125c4f9..e2f0412f6 100644 --- a/keep-ui/app/(keep)/alerts/alerts.tsx +++ b/keep-ui/app/(keep)/alerts/alerts.tsx @@ -19,6 +19,7 @@ import AlertChangeStatusModal from "./alert-change-status-modal"; import { useAlertPolling } from "utils/hooks/usePusher"; import NotFound from "@/app/(keep)/not-found"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; +import EnrichAlertSidePanel from "@/app/(keep)/alerts/EnrichAlertSidePanel"; const defaultPresets: Preset[] = [ { @@ -75,9 +76,9 @@ export default function Alerts({ presetName }: AlertsProps) { const ticketingProviders = useMemo( () => providersData.installed_providers.filter((provider) => - provider.tags.includes("ticketing") + provider.tags.includes("ticketing"), ), - [providersData.installed_providers] + [providersData.installed_providers], ); const searchParams = useSearchParams(); @@ -91,6 +92,9 @@ export default function Alerts({ presetName }: AlertsProps) { >(); const [changeStatusAlert, setChangeStatusAlert] = useState(); const [viewAlertModal, setViewAlertModal] = useState(); + const [viewEnrichAlertModal, setEnrichAlertModal] = + useState(); + const [isEnrichSidebarOpen, setIsEnrichSidebarOpen] = useState(false); const { useAllPresets } = usePresets(); const { data: savedPresets = [] } = useAllPresets({ @@ -99,7 +103,7 @@ export default function Alerts({ presetName }: AlertsProps) { const presets = [...defaultPresets, ...savedPresets] as const; const selectedPreset = presets.find( - (preset) => preset.name.toLowerCase() === decodeURIComponent(presetName) + (preset) => preset.name.toLowerCase() === decodeURIComponent(presetName), ); const { data: pollAlerts } = useAlertPolling(); @@ -112,14 +116,21 @@ export default function Alerts({ presetName }: AlertsProps) { const { status: sessionStatus } = useSession(); const isLoading = isAsyncLoading || sessionStatus === "loading"; - useEffect(() => { const fingerprint = searchParams?.get("alertPayloadFingerprint"); - if (fingerprint) { + const enrich = searchParams?.get("enrich"); + console.log(enrich, fingerprint); + if (fingerprint && enrich) { + const alert = alerts?.find((alert) => alert.fingerprint === fingerprint); + setEnrichAlertModal(alert); + setIsEnrichSidebarOpen(true); + } else if (fingerprint) { const alert = alerts?.find((alert) => alert.fingerprint === fingerprint); setViewAlertModal(alert); } else { setViewAlertModal(null); + setEnrichAlertModal(null); + setIsEnrichSidebarOpen(false); } }, [searchParams, alerts]); @@ -180,6 +191,15 @@ export default function Alerts({ presetName }: AlertsProps) { handleClose={() => router.replace(`/alerts/${presetName}`)} mutate={mutateAlerts} /> + { + setIsEnrichSidebarOpen(false); + router.replace(`/alerts/${presetName}`); + }} + mutate={mutateAlerts} + /> ); } diff --git a/keep-ui/components/SidePanel.tsx b/keep-ui/components/SidePanel.tsx index b0d6e10c9..b2ce13f87 100644 --- a/keep-ui/components/SidePanel.tsx +++ b/keep-ui/components/SidePanel.tsx @@ -39,7 +39,7 @@ const SidePanel: React.FC = ({ leaveFrom="translate-x-0" leaveTo="translate-x-full" > - + {children}