Skip to content

Commit

Permalink
feat: manually enrich alert (#2559)
Browse files Browse the repository at this point in the history
Signed-off-by: 35C4n0r <[email protected]>
Co-authored-by: Tal <[email protected]>
  • Loading branch information
35C4n0r and talboren authored Nov 25, 2024
1 parent 4d703b7 commit dd2bce0
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 6 deletions.
215 changes: 215 additions & 0 deletions keep-ui/app/(keep)/alerts/EnrichAlertSidePanel.tsx
Original file line number Diff line number Diff line change
@@ -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<EnrichAlertModalProps> = ({
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<Record<string, any>>({});
const [isDataValid, setIsDataValid] = useState<boolean>(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<string, string>,
);
};
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) => (
<div key={index} className="mb-4 flex items-center gap-2">
<TextInput
placeholder="Field Name"
value={field.key}
onChange={(e) => updateCustomField(index, "key", e.target.value)}
required
className="w-1/3"
/>
<TextInput
placeholder="Field Value"
value={field.value}
onChange={(e) => updateCustomField(index, "value", e.target.value)}
className="w-full"
/>
<Button color="red" onClick={() => removeCustomField(index)}>
</Button>
</div>
));

return (
<SidePanel isOpen={isOpen} onClose={handleClose} panelWidth={"w-1/3"}>
<div className="flex justify-between items-center min-w-full">
<h2 className="text-lg font-semibold">Enrich Alert</h2>
</div>

<div className="flex-1 overflow-auto pb-6 mt-4">
{renderCustomFields()}
</div>

<div className="sticky bottom-0 p-4 border-t border-gray-200 bg-white flex justify-end gap-2">
<Button
onClick={addCustomField}
className="bg-orange-500"
variant="primary"
>
+ Add Field
</Button>
<Button
onClick={handleSave}
color="orange"
variant="primary"
disabled={!isDataValid}
>
Save
</Button>
<Button onClick={handleClose} color="orange" variant="secondary">
Close
</Button>
</div>
</SidePanel>
);
};

export default EnrichAlertSidePanel;
19 changes: 19 additions & 0 deletions keep-ui/app/(keep)/alerts/alert-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
UserPlusIcon,
PlayIcon,
EyeIcon,
AdjustmentsHorizontalIcon,
} from "@heroicons/react/24/outline";
import { IoNotificationsOffOutline } from "react-icons/io5";

Expand Down Expand Up @@ -216,6 +217,24 @@ export default function AlertMenu({
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
onClick={() => {
router.replace(
`/alerts/${presetName}?alertPayloadFingerprint=${alert.fingerprint}&enrich=true`,
);
handleCloseMenu();
}}
className={`${
active ? "bg-slate-200" : "text-gray-900"
} group flex w-full items-center rounded-md px-2 py-2 text-xs`}
>
<AdjustmentsHorizontalIcon className="mr-2 h-4 w-4" aria-hidden="true" />
Enrich
</button>
)}
</Menu.Item>
{canAssign && (
<Menu.Item>
{({ active }) => (
Expand Down
30 changes: 25 additions & 5 deletions keep-ui/app/(keep)/alerts/alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
{
Expand Down Expand Up @@ -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();
Expand All @@ -91,6 +92,9 @@ export default function Alerts({ presetName }: AlertsProps) {
>();
const [changeStatusAlert, setChangeStatusAlert] = useState<AlertDto | null>();
const [viewAlertModal, setViewAlertModal] = useState<AlertDto | null>();
const [viewEnrichAlertModal, setEnrichAlertModal] =
useState<AlertDto | null>();
const [isEnrichSidebarOpen, setIsEnrichSidebarOpen] = useState(false);
const { useAllPresets } = usePresets();

const { data: savedPresets = [] } = useAllPresets({
Expand All @@ -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();
Expand All @@ -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]);

Expand Down Expand Up @@ -180,6 +191,15 @@ export default function Alerts({ presetName }: AlertsProps) {
handleClose={() => router.replace(`/alerts/${presetName}`)}
mutate={mutateAlerts}
/>
<EnrichAlertSidePanel
alert={viewEnrichAlertModal}
isOpen={isEnrichSidebarOpen}
handleClose={() => {
setIsEnrichSidebarOpen(false);
router.replace(`/alerts/${presetName}`);
}}
mutate={mutateAlerts}
/>
</>
);
}
2 changes: 1 addition & 1 deletion keep-ui/components/SidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const SidePanel: React.FC<SidePanelProps> = ({
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<Dialog.Panel className={`fixed right-0 inset-y-0 ${panelWidth} bg-white z-30 flex flex-col`}>
<Dialog.Panel className={`fixed right-0 inset-y-0 ${panelWidth} bg-white z-30 flex flex-col p-6`}>
{children}
</Dialog.Panel>
</Transition.Child>
Expand Down

0 comments on commit dd2bce0

Please sign in to comment.