From 193645834cfa3623dcd7e86eb6001dcd7c6caa5d Mon Sep 17 00:00:00 2001 From: Shahar Glazner Date: Tue, 26 Nov 2024 11:38:24 +0200 Subject: [PATCH] feat(ui): better presets modal (#2641) --- keep-ui/app/(keep)/alerts/alert-presets.tsx | 380 ++++++++++++++---- .../app/(keep)/alerts/alert-table-utils.tsx | 8 +- keep-ui/app/(keep)/alerts/alert-table.tsx | 18 +- keep-ui/shared/lib/server/getConfig.ts | 2 +- keep-ui/utils/hooks/useSearchAlerts.ts | 41 +- 5 files changed, 340 insertions(+), 109 deletions(-) diff --git a/keep-ui/app/(keep)/alerts/alert-presets.tsx b/keep-ui/app/(keep)/alerts/alert-presets.tsx index 217ff5fc2..fc8245043 100644 --- a/keep-ui/app/(keep)/alerts/alert-presets.tsx +++ b/keep-ui/app/(keep)/alerts/alert-presets.tsx @@ -1,8 +1,16 @@ import React, { useState, useEffect } from "react"; import { AlertDto, Preset } from "./models"; import Modal from "@/components/ui/Modal"; -import { Button, Subtitle, TextInput, Switch, Text } from "@tremor/react"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { + Button, + Badge, + Card, + Subtitle, + TextInput, + Switch, + Text, +} from "@tremor/react"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { toast } from "react-toastify"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { usePresets } from "utils/hooks/usePresets"; @@ -10,9 +18,24 @@ import { useTags } from "utils/hooks/useTags"; import { useRouter } from "next/navigation"; import { Table } from "@tanstack/react-table"; import { AlertsRulesBuilder } from "./alerts-rules-builder"; -import QueryBuilder, { formatQuery, parseCEL } from "react-querybuilder"; +import QueryBuilder, { + formatQuery, + parseCEL, + RuleGroupType, + DefaultRuleGroupType, +} from "react-querybuilder"; import CreatableMultiSelect from "@/components/ui/CreatableMultiSelect"; import { MultiValue } from "react-select"; +import { + useCopilotAction, + useCopilotContext, + useCopilotReadable, + CopilotTask, +} from "@copilotkit/react-core"; +import { TbSparkles } from "react-icons/tb"; +import { useSearchAlerts } from "utils/hooks/useSearchAlerts"; +import { Tooltip } from "@/shared/ui/Tooltip"; +import { InformationCircleIcon } from "@heroicons/react/24/outline"; type OptionType = { value: string; label: string }; @@ -29,6 +52,125 @@ interface Props { presetNoisy?: boolean; } +interface AlertsFoundBadgeProps { + alertsFound: AlertDto[] | undefined; // Updated to use AlertDto type + isLoading: boolean; + isDebouncing: boolean; + vertical?: boolean; +} + +export const AlertsFoundBadge: React.FC = ({ + alertsFound, + isLoading, + isDebouncing, + vertical = false, +}) => { + // Show loading state when searching or debouncing + if (isLoading || isDebouncing) { + return ( + +
+
+ + ... + + Searching... +
+
+
+ ); + } + + // Don't show anything if there's no data + if (!alertsFound) { + return null; + } + + return ( + +
+
+ + {alertsFound.length} + + + {alertsFound.length === 1 ? "Alert" : "Alerts"} found + +
+
+ + These are the alerts that would match your preset + +
+ ); +}; + +interface PresetControlsProps { + isPrivate: boolean; + setIsPrivate: (value: boolean) => void; + isNoisy: boolean; + setIsNoisy: (value: boolean) => void; +} + +export const PresetControls: React.FC = ({ + isPrivate, + setIsPrivate, + isNoisy, + setIsNoisy, +}) => { + return ( +
+
+
+ setIsPrivate(!isPrivate)} + color="orange" + /> + + Private presets are only visible to you} + className="z-50" + > + + +
+ +
+ setIsNoisy(!isNoisy)} + color="orange" + /> + + Noisy presets will trigger sound for every matching event + } + className="z-50" + > + + +
+
+
+ ); +}; + export default function AlertPresets({ presetNameFromApi, isLoading, @@ -54,8 +196,54 @@ export default function AlertPresets({ const [isPrivate, setIsPrivate] = useState(presetPrivate); const [isNoisy, setIsNoisy] = useState(presetNoisy); const [presetCEL, setPresetCEL] = useState(""); + + // Create + const defaultQuery: RuleGroupType = parseCEL(presetCEL) as RuleGroupType; + + // Parse CEL to RuleGroupType or use default empty rule group + const parsedQuery = presetCEL + ? (parseCEL(presetCEL) as RuleGroupType) + : defaultQuery; + + // Add useSearchAlerts hook with proper typing + const { data: alertsFound, isLoading: isSearching } = useSearchAlerts({ + query: parsedQuery, + timeframe: 0, + }); + + const [generatingName, setGeneratingName] = useState(false); const [selectedTags, setSelectedTags] = useState([]); const [newTags, setNewTags] = useState([]); // New tags created during the session + const { data: configData } = useConfig(); + + const isAIEnabled = configData?.OPEN_AI_API_KEY_SET; + const context = useCopilotContext(); + + useCopilotReadable({ + description: "The CEL query for the alert preset", + value: presetCEL, + }); + + useCopilotAction({ + name: "setGeneratedName", + description: "Set the generated preset name", + parameters: [ + { name: "name", type: "string", description: "The generated name" }, + ], + handler: async ({ name }) => { + setPresetName(name); + }, + }); + + const generatePresetName = async () => { + setGeneratingName(true); + const task = new CopilotTask({ + instructions: + "Generate a short, descriptive name for an alert preset based on the provided CEL query. The name should be concise but meaningful, reflecting the key conditions in the query.", + }); + await task.run(context); + setGeneratingName(false); + }; const selectedPreset = savedPresets.find( (savedPreset) => @@ -95,59 +283,59 @@ export default function AlertPresets({ } async function addOrUpdatePreset() { - if (presetName) { - // Translate the CEL to SQL - const sqlQuery = formatQuery(parseCEL(presetCEL), { - format: "parameterized_named", - parseNumbers: true, - }); + if (!presetName) return; + + const sqlQuery = formatQuery(parseCEL(presetCEL), { + format: "parameterized_named", + parseNumbers: true, + }); - const response = await fetch( - selectedPreset?.id - ? `${apiUrl}/preset/${selectedPreset?.id}` - : `${apiUrl}/preset`, + const response = await fetch( + selectedPreset?.id + ? `${apiUrl}/preset/${selectedPreset?.id}` + : `${apiUrl}/preset`, + { + method: selectedPreset?.id ? "PUT" : "POST", + headers: { + Authorization: `Bearer ${session?.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: presetName, + options: [ + { + label: "CEL", + value: presetCEL, + }, + { + label: "SQL", + value: sqlQuery, + }, + ], + is_private: isPrivate, + is_noisy: isNoisy, + tags: selectedTags.map((tag) => ({ + id: tag.id, + name: tag.name, + })), + }), + } + ); + + if (response.ok) { + setIsModalOpen(false); + await presetsMutator(); + await mutateTags(); + router.replace(`/alerts/${encodeURIComponent(presetName.toLowerCase())}`); + toast( + selectedPreset + ? `Preset ${presetName} updated!` + : `Preset ${presetName} created!`, { - method: selectedPreset?.id ? "PUT" : "POST", - headers: { - Authorization: `Bearer ${session?.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: presetName, - options: [ - { - label: "CEL", - value: presetCEL, - }, - { - label: "SQL", - value: sqlQuery, - }, - ], - is_private: isPrivate, - is_noisy: isNoisy, - tags: selectedTags.map((tag) => ({ - id: tag.id, - name: tag.name, - })), - }), + position: "top-left", + type: "success", } ); - if (response.ok) { - setIsModalOpen(false); - toast( - selectedPreset - ? `Preset ${presetName} updated!` - : `Preset ${presetName} created!`, - { - position: "top-left", - type: "success", - } - ); - presetsMutator(); - mutateTags(); - router.push(`/alerts/${presetName.toLowerCase()}`); - } } } @@ -168,11 +356,18 @@ export default function AlertPresets({ ); }; + // Handle modal close + const handleModalClose = () => { + setIsModalOpen(false); + setPresetName(""); + setPresetCEL(""); + }; + return ( <> setIsModalOpen(false)} + onClose={handleModalClose} className="w-[30%] max-w-screen-2xl max-h-[710px] transform overflow-auto ring-tremor bg-white p-6 text-left align-middle shadow-tremor transition-all rounded-xl" >
@@ -182,17 +377,39 @@ export default function AlertPresets({
Preset Name - setPresetName(e.target.value)} - className="w-full" +
+
+ setPresetName(e.target.value)} + className="w-full" + /> + {isAIEnabled && ( + + )} +
+
+
Tags @@ -210,35 +427,24 @@ export default function AlertPresets({ placeholder="Select or create tags" /> -
- setIsPrivate(!isPrivate)} - color={"orange"} - /> - -
-
- setIsNoisy(!isNoisy)} - color={"orange"} - /> - -
+ {/* Add alerts count card before the save buttons */} + {presetCEL && ( +
+ +
+ )}