From 815bbeb7a2443e8547f9bc130da0d8830ff3fb00 Mon Sep 17 00:00:00 2001 From: Shahar Glazner Date: Tue, 17 Dec 2024 11:56:49 +0200 Subject: [PATCH 01/16] fix(ui): add alert from UI (#2849) --- .../app/(keep)/alerts/alert-push-alert-to-server-modal.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/keep-ui/app/(keep)/alerts/alert-push-alert-to-server-modal.tsx b/keep-ui/app/(keep)/alerts/alert-push-alert-to-server-modal.tsx index f16ff6d73..457d1a42e 100644 --- a/keep-ui/app/(keep)/alerts/alert-push-alert-to-server-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-push-alert-to-server-modal.tsx @@ -80,6 +80,11 @@ const PushAlertToServerModal = ({ const onSubmit: SubmitHandler = async (data) => { try { + // if type is string, parse it to JSON + if (typeof data.alertJson === "string") { + data.alertJson = JSON.parse(data.alertJson); + } + const response = await api.post( `/alerts/event/${data.source.type}`, data.alertJson From d0aa75dcd9e3c1b7628c59a9cb0603fceeabfca7 Mon Sep 17 00:00:00 2001 From: Shahar Glazner Date: Tue, 17 Dec 2024 13:31:30 +0200 Subject: [PATCH 02/16] fix(api): gcp timeout (#2854) --- .../gcpmonitoring_provider.py | 78 +++++++++++++------ 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/keep/providers/gcpmonitoring_provider/gcpmonitoring_provider.py b/keep/providers/gcpmonitoring_provider/gcpmonitoring_provider.py index c055fd72f..bc6782e08 100644 --- a/keep/providers/gcpmonitoring_provider/gcpmonitoring_provider.py +++ b/keep/providers/gcpmonitoring_provider/gcpmonitoring_provider.py @@ -2,11 +2,13 @@ import datetime import json import logging +from typing import Optional import google.api_core import google.api_core.exceptions import google.cloud.logging import pydantic +from google.cloud import logging_v2 from keep.api.models.alert import AlertDto, AlertSeverity, AlertStatus from keep.contextmanager.contextmanager import ContextManager @@ -42,6 +44,13 @@ class GcpmonitoringProviderAuthConfig: "file_type": "application/json", # this is used to filter the file type in the UI } ) + timeout: Optional[int] = dataclasses.field( + default=30, + metadata={ + "required": False, + "description": "Query timeout in seconds", + }, + ) class GcpmonitoringProvider(BaseProvider): @@ -100,6 +109,7 @@ def __init__( self.authentication_config.service_account_json ) self._client = None + self.timeout = self.authentication_config.timeout def validate_config(self): self.authentication_config = GcpmonitoringProviderAuthConfig( @@ -107,13 +117,18 @@ def validate_config(self): ) def dispose(self): - pass + if self._client: + self._client.transport.close() def validate_scopes(self) -> dict[str, bool | str]: scopes = {} # try initializing the client to validate the scopes try: - self.client.list_entries(max_results=1) + # Use a small page size and timeout for validation + self.client.list_entries( + page_size=1, + timeout=10, + ) scopes["roles/logs.viewer"] = True except google.api_core.exceptions.PermissionDenied: scopes["roles/logs.viewer"] = ( @@ -131,7 +146,7 @@ def client(self) -> google.cloud.logging.Client: def __generate_client(self) -> google.cloud.logging.Client: if not self._client: - self._client = google.cloud.logging.Client.from_service_account_info( + self._client = logging_v2.Client.from_service_account_info( self._service_account_data ) return self._client @@ -143,9 +158,11 @@ def _query( page_size=1000, raw="true", project="", + timeout=None, **kwargs, ): raw = raw == "true" + timeout = timeout or self.timeout self.logger.info( f"Querying GCP Monitoring with filter: {filter} and timedelta_in_days: {timedelta_in_days}" ) @@ -159,25 +176,42 @@ def _query( if project: self.client.project = project - entries_iterator = self.client.list_entries(filter_=filter, page_size=page_size) - entries = [] - for entry in entries_iterator: - if raw: - entries.append(entry) - else: - try: - log_entry = LogEntry( - timestamp=entry.timestamp, - severity=entry.severity, - payload=entry.payload, - http_request=entry.http_request, - payload_exists=entry.payload is not None, - http_request_exists=entry.http_request is not None, - ) - entries.append(log_entry) - except Exception: - self.logger.error("Error parsing log entry") - continue + try: + self.logger.info(f"Querying logs with filter: {filter}") + entries_iterator = self.client.list_entries( + filter_=filter, + page_size=page_size, + timeout=timeout, + ) + self.logger.info("Querying logs completed") + entries = [] + for entry in entries_iterator: + if raw: + entries.append(entry) + else: + try: + log_entry = LogEntry( + timestamp=entry.timestamp, + severity=entry.severity, + payload=entry.payload, + http_request=entry.http_request, + payload_exists=entry.payload is not None, + http_request_exists=entry.http_request is not None, + ) + entries.append(log_entry) + except Exception: + self.logger.error("Error parsing log entry", exc_info=True) + continue + + self.logger.info(f"Found {len(entries)} entries") + return entries + + except google.api_core.exceptions.DeadlineExceeded: + self.logger.error(f"Query timed out after {timeout} seconds") + raise + except Exception as e: + self.logger.error(f"Error querying logs: {str(e)}", exc_info=True) + raise self.logger.info(f"Found {len(entries)} entries") return entries From 920c616d387aeabed0eda660791bd67fc18cf2fa Mon Sep 17 00:00:00 2001 From: Kirill Chernakov Date: Tue, 17 Dec 2024 17:02:20 +0400 Subject: [PATCH 03/16] fix: simplify presets updates (#2819) --- keep-ui/app/(keep)/alerts/alert-actions.tsx | 9 +- .../alerts/alert-change-status-modal.tsx | 7 +- .../app/(keep)/alerts/alert-dismiss-modal.tsx | 7 +- .../(keep)/alerts/alert-preset-manager.tsx | 96 ++++ keep-ui/app/(keep)/alerts/alert-presets.tsx | 467 ------------------ .../alert-push-alert-to-server-modal.tsx | 10 +- .../(keep)/alerts/alert-table-tab-panel.tsx | 5 +- keep-ui/app/(keep)/alerts/alert-table.tsx | 32 +- .../(keep)/alerts/alerts-rules-builder.tsx | 59 ++- keep-ui/app/(keep)/alerts/alerts.tsx | 92 ++-- keep-ui/app/(keep)/alerts/models.tsx | 24 +- keep-ui/app/(keep)/dashboard/GridLayout.tsx | 2 +- keep-ui/app/(keep)/dashboard/WidgetModal.tsx | 2 +- .../app/(keep)/dashboard/[id]/dashboard.tsx | 2 +- keep-ui/app/(keep)/dashboard/types.tsx | 2 +- .../CorrelationSidebarBody.tsx | 5 +- .../(keep)/settings/auth/permissions-tab.tsx | 5 +- .../topology/model/useTopologyApplications.ts | 2 +- keep-ui/components/navbar/AlertsLinks.tsx | 17 +- .../navbar/CustomPresetAlertLinks.tsx | 121 ++--- keep-ui/components/navbar/UserInfo.tsx | 4 +- keep-ui/entities/presets/model/constants.ts | 3 + keep-ui/entities/presets/model/types.ts | 36 ++ .../presets/model/usePresetActions.ts | 108 ++++ .../presets/model/usePresetPolling.ts | 43 ++ keep-ui/entities/presets/model/usePresets.ts | 142 ++++++ .../features/create-or-update-preset/index.ts | 1 + .../ui/alerts-count-badge.tsx | 80 +++ .../ui/create-or-update-preset-form.tsx | 234 +++++++++ .../ui/preset-controls.tsx | 61 +++ keep-ui/package-lock.json | 1 + keep-ui/package.json | 1 + keep-ui/shared/lib/state-utils.ts | 11 + keep-ui/shared/ui/utils/showSuccessToast.tsx | 5 + keep-ui/utils/hooks/useAlertPolling.ts | 48 ++ keep-ui/utils/hooks/useDashboardPresets.ts | 52 +- keep-ui/utils/hooks/usePresets.ts | 291 ----------- keep-ui/utils/hooks/usePusher.ts | 50 +- keep-ui/utils/hooks/useSearchAlerts.ts | 23 +- keep-ui/utils/hooks/useTags.ts | 3 +- keep-ui/utils/state.ts | 9 - keep/api/tasks/process_event_task.py | 33 +- 42 files changed, 1048 insertions(+), 1157 deletions(-) create mode 100644 keep-ui/app/(keep)/alerts/alert-preset-manager.tsx delete mode 100644 keep-ui/app/(keep)/alerts/alert-presets.tsx create mode 100644 keep-ui/entities/presets/model/constants.ts create mode 100644 keep-ui/entities/presets/model/types.ts create mode 100644 keep-ui/entities/presets/model/usePresetActions.ts create mode 100644 keep-ui/entities/presets/model/usePresetPolling.ts create mode 100644 keep-ui/entities/presets/model/usePresets.ts create mode 100644 keep-ui/features/create-or-update-preset/index.ts create mode 100644 keep-ui/features/create-or-update-preset/ui/alerts-count-badge.tsx create mode 100644 keep-ui/features/create-or-update-preset/ui/create-or-update-preset-form.tsx create mode 100644 keep-ui/features/create-or-update-preset/ui/preset-controls.tsx create mode 100644 keep-ui/shared/lib/state-utils.ts create mode 100644 keep-ui/shared/ui/utils/showSuccessToast.tsx create mode 100644 keep-ui/utils/hooks/useAlertPolling.ts delete mode 100644 keep-ui/utils/hooks/usePresets.ts delete mode 100644 keep-ui/utils/state.ts diff --git a/keep-ui/app/(keep)/alerts/alert-actions.tsx b/keep-ui/app/(keep)/alerts/alert-actions.tsx index bb0a0d6db..ccbba1398 100644 --- a/keep-ui/app/(keep)/alerts/alert-actions.tsx +++ b/keep-ui/app/(keep)/alerts/alert-actions.tsx @@ -3,13 +3,14 @@ import { Button } from "@tremor/react"; import { AlertDto } from "./models"; import { PlusIcon, RocketIcon } from "@radix-ui/react-icons"; import { toast } from "react-toastify"; -import { usePresets } from "utils/hooks/usePresets"; import { useRouter } from "next/navigation"; import { SilencedDoorbellNotification } from "@/components/icons"; import AlertAssociateIncidentModal from "./alert-associate-incident-modal"; import CreateIncidentWithAIModal from "./alert-create-incident-ai-modal"; import { useApi } from "@/shared/lib/hooks/useApi"; +import { useRevalidateMultiple } from "@/shared/lib/state-utils"; + interface Props { selectedRowIds: string[]; alerts: AlertDto[]; @@ -30,11 +31,9 @@ export default function AlertActions({ isIncidentSelectorOpen, }: Props) { const router = useRouter(); - const { useAllPresets } = usePresets(); const api = useApi(); - const { mutate: presetsMutator } = useAllPresets({ - revalidateOnFocus: false, - }); + const revalidateMultiple = useRevalidateMultiple(); + const presetsMutator = () => revalidateMultiple(["/preset"]); const [isCreateIncidentWithAIOpen, setIsCreateIncidentWithAIOpen] = useState(false); diff --git a/keep-ui/app/(keep)/alerts/alert-change-status-modal.tsx b/keep-ui/app/(keep)/alerts/alert-change-status-modal.tsx index 561d7204a..7218af97e 100644 --- a/keep-ui/app/(keep)/alerts/alert-change-status-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-change-status-modal.tsx @@ -16,11 +16,12 @@ import { XCircleIcon, QuestionMarkCircleIcon, } from "@heroicons/react/24/outline"; -import { usePresets } from "utils/hooks/usePresets"; import { useAlerts } from "utils/hooks/useAlerts"; import { useApi } from "@/shared/lib/hooks/useApi"; import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { useRevalidateMultiple } from "@/shared/lib/state-utils"; + const statusIcons = { [Status.Firing]: , [Status.Resolved]: , @@ -76,8 +77,8 @@ export default function AlertChangeStatusModal({ }: Props) { const api = useApi(); const [selectedStatus, setSelectedStatus] = useState(null); - const { useAllPresets } = usePresets(); - const { mutate: presetsMutator } = useAllPresets(); + const revalidateMultiple = useRevalidateMultiple(); + const presetsMutator = () => revalidateMultiple(["/preset"]); const { useAllAlerts } = useAlerts(); const { mutate: alertsMutator } = useAllAlerts(presetName, { revalidateOnMount: false, diff --git a/keep-ui/app/(keep)/alerts/alert-dismiss-modal.tsx b/keep-ui/app/(keep)/alerts/alert-dismiss-modal.tsx index af30eb28e..3b161f628 100644 --- a/keep-ui/app/(keep)/alerts/alert-dismiss-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-dismiss-modal.tsx @@ -16,7 +16,6 @@ import "react-datepicker/dist/react-datepicker.css"; import "react-quill/dist/quill.snow.css"; import { AlertDto } from "./models"; import { set, isSameDay, isAfter } from "date-fns"; -import { usePresets } from "utils/hooks/usePresets"; import { useAlerts } from "utils/hooks/useAlerts"; import { toast } from "react-toastify"; const ReactQuill = @@ -25,6 +24,8 @@ import "./alert-dismiss-modal.css"; import { useApi } from "@/shared/lib/hooks/useApi"; import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { useRevalidateMultiple } from "@/shared/lib/state-utils"; + interface Props { preset: string; alert: AlertDto[] | null | undefined; @@ -41,8 +42,8 @@ export default function AlertDismissModal({ const [selectedDateTime, setSelectedDateTime] = useState(null); const [showError, setShowError] = useState(false); - const { useAllPresets } = usePresets(); - const { mutate: presetsMutator } = useAllPresets(); + const revalidateMultiple = useRevalidateMultiple(); + const presetsMutator = () => revalidateMultiple(["/preset"]); const { usePresetAlerts } = useAlerts(); const { mutate: alertsMutator } = usePresetAlerts(presetName, { revalidateOnMount: false, diff --git a/keep-ui/app/(keep)/alerts/alert-preset-manager.tsx b/keep-ui/app/(keep)/alerts/alert-preset-manager.tsx new file mode 100644 index 000000000..f07ecf2dc --- /dev/null +++ b/keep-ui/app/(keep)/alerts/alert-preset-manager.tsx @@ -0,0 +1,96 @@ +import React, { useMemo, useState } from "react"; +import { AlertDto } from "./models"; +import Modal from "@/components/ui/Modal"; +import { useRouter } from "next/navigation"; +import { Table } from "@tanstack/react-table"; +import { AlertsRulesBuilder } from "./alerts-rules-builder"; +import { CreateOrUpdatePresetForm } from "@/features/create-or-update-preset"; +import { STATIC_PRESETS_NAMES } from "@/entities/presets/model/constants"; +import { Preset } from "@/entities/presets/model/types"; +import { usePresets } from "@/entities/presets/model/usePresets"; +import { CopilotKit } from "@copilotkit/react-core"; + +interface Props { + presetName: string; + // TODO: pass specific functions not the whole table? + table: Table; +} + +export function AlertPresetManager({ presetName, table }: Props) { + const { dynamicPresets } = usePresets({ + revalidateOnFocus: false, + }); + // TODO: make a hook for this? store in the context? + const selectedPreset = useMemo(() => { + return dynamicPresets?.find( + (p) => + p.name.toLowerCase() === decodeURIComponent(presetName).toLowerCase() + ) as Preset | undefined; + }, [dynamicPresets, presetName]); + const [presetCEL, setPresetCEL] = useState(""); + + // modal + const [isModalOpen, setIsModalOpen] = useState(false); + const router = useRouter(); + + const onCreateOrUpdatePreset = (preset: Preset) => { + setIsModalOpen(false); + const encodedPresetName = encodeURIComponent(preset.name.toLowerCase()); + router.push(`/alerts/${encodedPresetName}`); + }; + + const handleModalClose = () => { + setIsModalOpen(false); + }; + + const isDynamic = + selectedPreset && !STATIC_PRESETS_NAMES.includes(selectedPreset.name); + + // Static presets are not editable + const idToUpdate = isDynamic ? selectedPreset.id : null; + + const presetData = isDynamic + ? { + CEL: presetCEL, + name: selectedPreset.name, + isPrivate: selectedPreset.is_private, + isNoisy: selectedPreset.is_noisy, + tags: selectedPreset.tags, + } + : { + CEL: presetCEL, + name: undefined, + isPrivate: undefined, + isNoisy: undefined, + tags: undefined, + }; + + return ( + <> +
+ +
+ + + + + + + ); +} diff --git a/keep-ui/app/(keep)/alerts/alert-presets.tsx b/keep-ui/app/(keep)/alerts/alert-presets.tsx deleted file mode 100644 index d9aa4ce7e..000000000 --- a/keep-ui/app/(keep)/alerts/alert-presets.tsx +++ /dev/null @@ -1,467 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { AlertDto, Preset } from "./models"; -import Modal from "@/components/ui/Modal"; -import { - Button, - Badge, - Card, - Subtitle, - TextInput, - Switch, - Text, -} from "@tremor/react"; -import { useConfig } from "utils/hooks/useConfig"; -import { toast } from "react-toastify"; -import { usePresets } from "utils/hooks/usePresets"; -import { useTags } from "utils/hooks/useTags"; -import { useRouter } from "next/navigation"; -import { Table } from "@tanstack/react-table"; -import { AlertsRulesBuilder } from "./alerts-rules-builder"; -import { formatQuery, parseCEL, RuleGroupType } from "react-querybuilder"; -import { useApi } from "@/shared/lib/hooks/useApi"; -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"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; - -interface TagOption { - id?: number; - name: string; -} - -interface Props { - presetNameFromApi: string; - isLoading: boolean; - table: Table; - presetPrivate?: boolean; - 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, - table, - presetPrivate = false, - presetNoisy = false, -}: Props) { - const api = useApi(); - const { useAllPresets } = usePresets(); - const { mutate: presetsMutator, data: savedPresets = [] } = useAllPresets({ - revalidateOnFocus: false, - }); - const { data: tags = [], mutate: mutateTags } = useTags(); - const router = useRouter(); - - const [isModalOpen, setIsModalOpen] = useState(false); - const [presetName, setPresetName] = useState( - presetNameFromApi === "feed" || presetNameFromApi === "deleted" - ? "" - : presetNameFromApi - ); - 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) => - savedPreset.name.toLowerCase() === - decodeURIComponent(presetNameFromApi).toLowerCase() - ) as Preset | undefined; - - useEffect(() => { - if (selectedPreset) { - setSelectedTags( - selectedPreset.tags.map((tag) => ({ id: tag.id, name: tag.name })) - ); - } - }, [selectedPreset]); - - async function deletePreset(presetId: string) { - if ( - confirm( - `You are about to delete preset ${presetNameFromApi}, are you sure?` - ) - ) { - try { - const response = await api.delete(`/preset/${presetId}`); - toast(`Preset ${presetNameFromApi} deleted!`, { - position: "top-left", - type: "success", - }); - presetsMutator(); - router.push("/alerts/feed"); - } catch (error) { - toast(`Error deleting preset ${presetNameFromApi}`, { - position: "top-left", - type: "error", - }); - } - } - } - - async function addOrUpdatePreset() { - if (!presetName) return; - - let sqlQuery; - try { - sqlQuery = formatQuery(parseCEL(presetCEL), { - format: "parameterized_named", - parseNumbers: true, - }); - } catch (error) { - showErrorToast(error, "Failed to parse the CEL query"); - return; - } - const body = { - 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, - })), - }; - - try { - const response = selectedPreset?.id - ? await api.put(`/preset/${selectedPreset?.id}`, body) - : await api.post(`/preset`, body); - setIsModalOpen(false); - await presetsMutator(); - await mutateTags(); - router.replace(`/alerts/${encodeURIComponent(presetName.toLowerCase())}`); - toast( - selectedPreset - ? `Preset ${presetName} updated!` - : `Preset ${presetName} created!`, - { - position: "top-left", - type: "success", - } - ); - } catch (error) { - showErrorToast(error, "Failed to update preset"); - } - } - - const handleCreateTag = (inputValue: string) => { - const newTag = { name: inputValue }; - setNewTags((prevTags) => [...prevTags, inputValue]); - setSelectedTags((prevTags) => [...prevTags, newTag]); - }; - - const handleChange = ( - newValue: MultiValue<{ value: string; label: string }> - ) => { - setSelectedTags( - newValue.map((tag) => ({ - id: tags.find((t) => t.name === tag.value)?.id, - name: tag.value, - })) - ); - }; - - // Handle modal close - const handleModalClose = () => { - setIsModalOpen(false); - setPresetName(""); - setPresetCEL(""); - }; - - return ( - <> - -
-
-

{presetName ? "Update preset" : "Enter new preset name"}

-
- -
- Preset Name -
-
- setPresetName(e.target.value)} - className="w-full" - /> - {isAIEnabled && ( - - )} -
-
- -
- Tags - ({ - value: tag.name, - label: tag.name, - }))} - onChange={handleChange} - onCreateOption={handleCreateTag} - options={tags.map((tag) => ({ - value: tag.name, - label: tag.name, - }))} - placeholder="Select or create tags" - /> - - {/* Add alerts count card before the save buttons */} - {presetCEL && ( -
- -
- )} - -
- - -
-
-
-
- -
- - ); -} diff --git a/keep-ui/app/(keep)/alerts/alert-push-alert-to-server-modal.tsx b/keep-ui/app/(keep)/alerts/alert-push-alert-to-server-modal.tsx index 457d1a42e..d5aedf09e 100644 --- a/keep-ui/app/(keep)/alerts/alert-push-alert-to-server-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-push-alert-to-server-modal.tsx @@ -10,11 +10,12 @@ import Modal from "@/components/ui/Modal"; import { useProviders } from "utils/hooks/useProviders"; import ImageWithFallback from "@/components/ImageWithFallback"; import { useAlerts } from "utils/hooks/useAlerts"; -import { usePresets } from "utils/hooks/usePresets"; import Select from "@/components/ui/Select"; import { useApi } from "@/shared/lib/hooks/useApi"; import { KeepApiError } from "@/shared/api"; +import { useRevalidateMultiple } from "@/shared/lib/state-utils"; + interface PushAlertToServerModalProps { handleClose: () => void; presetName: string; @@ -31,11 +32,8 @@ const PushAlertToServerModal = ({ presetName, }: PushAlertToServerModalProps) => { const [alertSources, setAlertSources] = useState([]); - const { useAllPresets } = usePresets(); - const { mutate: presetsMutator } = useAllPresets({ - revalidateIfStale: false, - revalidateOnFocus: false, - }); + const revalidateMultiple = useRevalidateMultiple(); + const presetsMutator = () => revalidateMultiple(["/preset"]); const { usePresetAlerts } = useAlerts(); const { mutate: mutateAlerts } = usePresetAlerts(presetName); diff --git a/keep-ui/app/(keep)/alerts/alert-table-tab-panel.tsx b/keep-ui/app/(keep)/alerts/alert-table-tab-panel.tsx index e4f5310b5..372ca62b8 100644 --- a/keep-ui/app/(keep)/alerts/alert-table-tab-panel.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table-tab-panel.tsx @@ -1,6 +1,7 @@ import { AlertTable } from "./alert-table"; import { useAlertTableCols } from "./alert-table-utils"; -import { AlertDto, AlertKnownKeys, Preset, getTabsFromPreset } from "./models"; +import { AlertDto, AlertKnownKeys, getTabsFromPreset } from "./models"; +import { Preset } from "@/entities/presets/model/types"; interface Props { alerts: AlertDto[]; @@ -83,8 +84,6 @@ export default function AlertTableTabPanel({ setDismissedModalAlert={setDismissModalAlert} isAsyncLoading={isAsyncLoading} presetName={preset.name} - presetPrivate={preset.is_private} - presetNoisy={preset.is_noisy} presetStatic={preset.name === "feed"} presetId={preset.id} presetTabs={presetTabs} diff --git a/keep-ui/app/(keep)/alerts/alert-table.tsx b/keep-ui/app/(keep)/alerts/alert-table.tsx index 4c98140da..a15e58a6d 100644 --- a/keep-ui/app/(keep)/alerts/alert-table.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table.tsx @@ -14,7 +14,6 @@ import { SortingState, getSortedRowModel, } from "@tanstack/react-table"; -import { CopilotKit } from "@copilotkit/react-core"; import AlertPagination from "./alert-pagination"; import AlertsTableHeaders from "./alert-table-headers"; import { useLocalStorage } from "utils/hooks/useLocalStorage"; @@ -25,7 +24,7 @@ import { DEFAULT_COLS, } from "./alert-table-utils"; import AlertActions from "./alert-actions"; -import AlertPresets from "./alert-presets"; +import { AlertPresetManager } from "./alert-preset-manager"; import { evalWithContext } from "./alerts-rules-builder"; import { TitleAndFilters } from "./TitleAndFilters"; import { severityMapping } from "./models"; @@ -33,6 +32,7 @@ import AlertTabs from "./alert-tabs"; import AlertSidebar from "./alert-sidebar"; import { AlertFacets } from "./alert-table-alert-facets"; import { DynamicFacet, FacetFilters } from "./alert-table-facet-types"; +import { useConfig } from "@/utils/hooks/useConfig"; interface PresetTab { name: string; @@ -44,8 +44,6 @@ interface Props { columns: ColumnDef[]; isAsyncLoading?: boolean; presetName: string; - presetPrivate?: boolean; - presetNoisy?: boolean; presetStatic?: boolean; presetId?: string; presetTabs?: PresetTab[]; @@ -63,8 +61,6 @@ export function AlertTable({ columns, isAsyncLoading = false, presetName, - presetPrivate = false, - presetNoisy = false, presetStatic = false, presetId = "", presetTabs = [], @@ -76,6 +72,8 @@ export function AlertTable({ setChangeStatusAlert, }: Props) { const a11yContainerRef = useRef(null); + const { data: configData } = useConfig(); + const noisyAlertsEnabled = configData?.NOISY_ALERTS_ENABLED; const [theme, setTheme] = useLocalStorage( "alert-table-theme", @@ -137,9 +135,9 @@ export function AlertTable({ setTheme(newTheme); }; - const [sorting, setSorting] = useState([ - { id: "noise", desc: true }, - ]); + const [sorting, setSorting] = useState( + noisyAlertsEnabled ? [{ id: "noise", desc: true }] : [] + ); const [tabs, setTabs] = useState([ { name: "All", filter: (alert: AlertDto) => true }, @@ -218,6 +216,10 @@ export function AlertTable({ }); }); + const leftPinnedColumns = noisyAlertsEnabled + ? ["severity", "checkbox", "noise"] + : ["severity", "checkbox"]; + const table = useReactTable({ data: filteredAlerts, columns: columns, @@ -226,7 +228,7 @@ export function AlertTable({ columnOrder: columnOrder, columnSizing: columnSizing, columnPinning: { - left: ["severity", "checkbox", "noise"], + left: leftPinnedColumns, right: ["alertMenu"], }, sorting: sorting, @@ -296,15 +298,7 @@ export function AlertTable({ isIncidentSelectorOpen={isIncidentSelectorOpen} /> ) : ( - - - + )} diff --git a/keep-ui/app/(keep)/alerts/alerts-rules-builder.tsx b/keep-ui/app/(keep)/alerts/alerts-rules-builder.tsx index c6d6a2967..3ddf2c0ee 100644 --- a/keep-ui/app/(keep)/alerts/alerts-rules-builder.tsx +++ b/keep-ui/app/(keep)/alerts/alerts-rules-builder.tsx @@ -12,12 +12,7 @@ import QueryBuilder, { } from "react-querybuilder"; import "react-querybuilder/dist/query-builder.scss"; import { Table } from "@tanstack/react-table"; -import { - AlertDto, - Preset, - severityMapping, - reverseSeverityMapping, -} from "./models"; +import { AlertDto, severityMapping, reverseSeverityMapping } from "./models"; import { XMarkIcon, TrashIcon } from "@heroicons/react/24/outline"; import { TbDatabaseImport } from "react-icons/tb"; import Select, { components, MenuListProps } from "react-select"; @@ -29,6 +24,9 @@ import { toast } from "react-toastify"; import { CornerDownLeft } from "lucide-react"; import { Link } from "@/components/ui"; import { DocumentTextIcon } from "@heroicons/react/24/outline"; +import { STATIC_PRESETS_NAMES } from "@/entities/presets/model/constants"; +import { Preset } from "@/entities/presets/model/types"; +import { usePresetActions } from "@/entities/presets/model/usePresetActions"; const staticOptions = [ { value: 'severity > "info"', label: 'severity > "info"' }, @@ -279,7 +277,6 @@ type AlertsRulesBuilderProps = { selectedPreset?: Preset; defaultQuery: string | undefined; setIsModalOpen?: React.Dispatch>; - deletePreset?: (presetId: string) => Promise; setPresetCEL?: React.Dispatch>; updateOutputCEL?: React.Dispatch>; showSqlImport?: boolean; @@ -299,7 +296,6 @@ export const AlertsRulesBuilder = ({ selectedPreset, defaultQuery = "", setIsModalOpen, - deletePreset, setPresetCEL, updateOutputCEL, customFields, @@ -313,6 +309,8 @@ export const AlertsRulesBuilder = ({ const pathname = usePathname(); const searchParams = useSearchParams(); + const { deletePreset } = usePresetActions(); + const [isGUIOpen, setIsGUIOpen] = useState(false); const [isImportSQLOpen, setImportSQLOpen] = useState(false); const [sqlQuery, setSQLQuery] = useState(""); @@ -321,6 +319,11 @@ export const AlertsRulesBuilder = ({ ); const parsedCELRulesToQuery = parseCEL(celRules); + const isDynamic = + selectedPreset && !STATIC_PRESETS_NAMES.includes(selectedPreset.name); + + const action = isDynamic ? "update" : "create"; + const setQueryParam = (key: string, value: string) => { const current = new URLSearchParams( Array.from(searchParams ? searchParams.entries() : []) @@ -505,8 +508,8 @@ export const AlertsRulesBuilder = ({ operators: getOperators(id), })) : customFields - ? customFields - : []; + ? customFields + : []; const onImportSQL = () => { setImportSQLOpen(true); @@ -648,24 +651,30 @@ export const AlertsRulesBuilder = ({ size="sm" disabled={!celRules.length} onClick={() => validateAndOpenSaveModal(celRules)} - tooltip="Save current filter as a preset" + tooltip={ + action === "update" + ? "Edit preset" + : "Save current filter as a preset" + } > - Save + {action === "update" ? "Edit" : "Save"} )} - {selectedPreset && - selectedPreset.name && - selectedPreset?.name !== "deleted" && - selectedPreset?.name !== "feed" && - selectedPreset?.name !== "dismissed" && - deletePreset && ( - - )} + {isDynamic && ( + + )} {/* Import SQL */} diff --git a/keep-ui/app/(keep)/alerts/alerts.tsx b/keep-ui/app/(keep)/alerts/alerts.tsx index e63c7fab7..b40347549 100644 --- a/keep-ui/app/(keep)/alerts/alerts.tsx +++ b/keep-ui/app/(keep)/alerts/alerts.tsx @@ -1,9 +1,8 @@ "use client"; import { useEffect, useMemo, useState } from "react"; -import { Preset } from "./models"; import { useAlerts } from "utils/hooks/useAlerts"; -import { usePresets } from "utils/hooks/usePresets"; +import { usePresets } from "@/entities/presets/model/usePresets"; import AlertTableTabPanel from "./alert-table-tab-panel"; import { AlertHistory } from "./alert-history"; import AlertAssignTicketModal from "./alert-assign-ticket-modal"; @@ -16,10 +15,12 @@ import AlertDismissModal from "./alert-dismiss-modal"; import { ViewAlertModal } from "./ViewAlertModal"; import { useRouter, useSearchParams } from "next/navigation"; import AlertChangeStatusModal from "./alert-change-status-modal"; -import { useAlertPolling } from "utils/hooks/usePusher"; import NotFound from "@/app/(keep)/not-found"; import { useApi } from "@/shared/lib/hooks/useApi"; import EnrichAlertSidePanel from "@/app/(keep)/alerts/EnrichAlertSidePanel"; +import Loading from "../loading"; +import { Preset } from "@/entities/presets/model/types"; +import { useAlertPolling } from "@/utils/hooks/useAlertPolling"; const defaultPresets: Preset[] = [ { @@ -32,36 +33,6 @@ const defaultPresets: Preset[] = [ should_do_noise_now: false, tags: [], }, - { - id: "dismissed", - name: "dismissed", - options: [], - is_private: false, - is_noisy: false, - alerts_count: 0, - should_do_noise_now: false, - tags: [], - }, - { - id: "groups", - name: "groups", - options: [], - is_private: false, - is_noisy: false, - alerts_count: 0, - should_do_noise_now: false, - tags: [], - }, - { - id: "without-incident", - name: "without-incident", - options: [], - is_private: false, - is_noisy: false, - alerts_count: 0, - should_do_noise_now: false, - tags: [], - }, ]; type AlertsProps = { @@ -69,6 +40,7 @@ type AlertsProps = { }; export default function Alerts({ presetName }: AlertsProps) { + const api = useApi(); const { usePresetAlerts } = useAlerts(); const { data: providersData = { installed_providers: [] } } = useProviders(); const router = useRouter(); @@ -76,9 +48,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(); @@ -95,15 +67,15 @@ export default function Alerts({ presetName }: AlertsProps) { const [viewEnrichAlertModal, setEnrichAlertModal] = useState(); const [isEnrichSidebarOpen, setIsEnrichSidebarOpen] = useState(false); - const { useAllPresets } = usePresets(); - - const { data: savedPresets = [] } = useAllPresets({ - revalidateOnFocus: false, - }); + const { dynamicPresets: savedPresets = [], isLoading: _isPresetsLoading } = + usePresets({ + revalidateOnFocus: false, + }); + const isPresetsLoading = _isPresetsLoading || !api.isReady(); 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(); @@ -114,13 +86,11 @@ export default function Alerts({ presetName }: AlertsProps) { error: alertsError, } = usePresetAlerts(selectedPreset ? selectedPreset.name : ""); - const api = useApi(); const isLoading = isAsyncLoading || !api.isReady(); useEffect(() => { const fingerprint = searchParams?.get("alertPayloadFingerprint"); const enrich = searchParams?.get("enrich"); - console.log(enrich, fingerprint); if (fingerprint && enrich) { const alert = alerts?.find((alert) => alert.fingerprint === fingerprint); setEnrichAlertModal(alert); @@ -141,6 +111,16 @@ export default function Alerts({ presetName }: AlertsProps) { } }, [mutateAlerts, pollAlerts]); + // if we don't have presets data yet, just show loading + if (!selectedPreset && isPresetsLoading) { + return ; + } + + // if we have an error, throw it, error.tsx will catch it + if (alertsError) { + throw alertsError; + } + if (!selectedPreset) { return ; } @@ -160,9 +140,18 @@ export default function Alerts({ presetName }: AlertsProps) { mutateAlerts={mutateAlerts} /> - {selectedPreset && ( - - )} + + setDismissModalAlert(null)} + /> + setChangeStatusAlert(null)} + /> + setTicketModalAlert(null)} ticketingProviders={ticketingProviders} @@ -172,21 +161,10 @@ export default function Alerts({ presetName }: AlertsProps) { handleClose={() => setNoteModalAlert(null)} alert={noteModalAlert ?? null} /> - {selectedPreset && } setRunWorkflowModalAlert(null)} /> - setDismissModalAlert(null)} - /> - setChangeStatusAlert(null)} - /> router.replace(`/alerts/${presetName}`)} diff --git a/keep-ui/app/(keep)/alerts/models.tsx b/keep-ui/app/(keep)/alerts/models.tsx index a7bbfb5d5..0077f4629 100644 --- a/keep-ui/app/(keep)/alerts/models.tsx +++ b/keep-ui/app/(keep)/alerts/models.tsx @@ -1,3 +1,5 @@ +import { Preset } from "@/entities/presets/model/types"; + export enum Severity { Critical = "critical", High = "high", @@ -66,28 +68,6 @@ export interface AlertDto { is_created_by_ai?: boolean; } -interface Option { - readonly label: string; - readonly value: string; -} - -export interface Tag { - id: number; - name: string; -} - -export interface Preset { - id: string; - name: string; - options: Option[]; - is_private: boolean; - is_noisy: boolean; - should_do_noise_now: boolean; - alerts_count: number; - created_by?: string; - tags: Tag[]; -} - export function getTabsFromPreset(preset: Preset): any[] { const tabsOption = preset.options.find( (option) => option.label.toLowerCase() === "tabs" diff --git a/keep-ui/app/(keep)/dashboard/GridLayout.tsx b/keep-ui/app/(keep)/dashboard/GridLayout.tsx index 20f4e7d56..bcee98a4f 100644 --- a/keep-ui/app/(keep)/dashboard/GridLayout.tsx +++ b/keep-ui/app/(keep)/dashboard/GridLayout.tsx @@ -3,8 +3,8 @@ import { Responsive, WidthProvider, Layout } from "react-grid-layout"; import GridItemContainer from "./GridItemContainer"; import { LayoutItem, WidgetData } from "./types"; import "react-grid-layout/css/styles.css"; -import { Preset } from "@/app/(keep)/alerts/models"; import { MetricsWidget } from "@/utils/hooks/useDashboardMetricWidgets"; +import { Preset } from "@/entities/presets/model/types"; const ResponsiveGridLayout = WidthProvider(Responsive); diff --git a/keep-ui/app/(keep)/dashboard/WidgetModal.tsx b/keep-ui/app/(keep)/dashboard/WidgetModal.tsx index 81ed554ec..d82fa3a78 100644 --- a/keep-ui/app/(keep)/dashboard/WidgetModal.tsx +++ b/keep-ui/app/(keep)/dashboard/WidgetModal.tsx @@ -10,9 +10,9 @@ import { } from "@tremor/react"; import { Trashcan } from "components/icons"; import { GenericsMetrics, Threshold, WidgetData, WidgetType } from "./types"; -import { Preset } from "@/app/(keep)/alerts/models"; import { Controller, get, useForm, useWatch } from "react-hook-form"; import { MetricsWidget } from "@/utils/hooks/useDashboardMetricWidgets"; +import { Preset } from "@/entities/presets/model/types"; interface WidgetForm { widgetName: string; diff --git a/keep-ui/app/(keep)/dashboard/[id]/dashboard.tsx b/keep-ui/app/(keep)/dashboard/[id]/dashboard.tsx index 6569184d1..0febd5439 100644 --- a/keep-ui/app/(keep)/dashboard/[id]/dashboard.tsx +++ b/keep-ui/app/(keep)/dashboard/[id]/dashboard.tsx @@ -11,7 +11,6 @@ import { WidgetData, WidgetType, } from "../types"; -import { Preset } from "@/app/(keep)/alerts/models"; import { FiEdit2, FiSave } from "react-icons/fi"; import { useDashboards } from "utils/hooks/useDashboards"; import { toast } from "react-toastify"; @@ -24,6 +23,7 @@ import { import { useApi } from "@/shared/lib/hooks/useApi"; import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; import "../styles.css"; +import { Preset } from "@/entities/presets/model/types"; const DASHBOARD_FILTERS = [ { diff --git a/keep-ui/app/(keep)/dashboard/types.tsx b/keep-ui/app/(keep)/dashboard/types.tsx index 2de15c968..42f9851df 100644 --- a/keep-ui/app/(keep)/dashboard/types.tsx +++ b/keep-ui/app/(keep)/dashboard/types.tsx @@ -1,5 +1,5 @@ -import { Preset } from "@/app/(keep)/alerts/models"; import { MetricsWidget } from "@/utils/hooks/useDashboardMetricWidgets"; +import { Preset } from "@/entities/presets/model/types"; export interface LayoutItem { i: string; diff --git a/keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationSidebarBody.tsx b/keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationSidebarBody.tsx index 4699d58a0..4ad2e1021 100644 --- a/keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationSidebarBody.tsx +++ b/keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationSidebarBody.tsx @@ -121,10 +121,7 @@ export const CorrelationSidebarBody = ({ )} -
+
diff --git a/keep-ui/app/(keep)/settings/auth/permissions-tab.tsx b/keep-ui/app/(keep)/settings/auth/permissions-tab.tsx index b1848366f..1e24c6160 100644 --- a/keep-ui/app/(keep)/settings/auth/permissions-tab.tsx +++ b/keep-ui/app/(keep)/settings/auth/permissions-tab.tsx @@ -4,7 +4,7 @@ import { usePermissions } from "utils/hooks/usePermissions"; import { useUsers } from "@/entities/users/model/useUsers"; import { useGroups } from "utils/hooks/useGroups"; import { useRoles } from "utils/hooks/useRoles"; -import { usePresets } from "utils/hooks/usePresets"; +import { usePresets } from "@/entities/presets/model/usePresets"; import { useIncidents } from "utils/hooks/useIncidents"; import Loading from "@/app/(keep)/loading"; import { PermissionsTable } from "./permissions-table"; @@ -38,8 +38,7 @@ export default function PermissionsTab({ isDisabled = false }: Props) { const { data: users } = useUsers(); const { data: groups } = useGroups(); const { data: roles } = useRoles(); - const { useAllPresets } = usePresets(); - const { data: presets } = useAllPresets(); + const { dynamicPresets: presets } = usePresets(); const { data: incidents } = useIncidents(); const [loading, setLoading] = useState(true); diff --git a/keep-ui/app/(keep)/topology/model/useTopologyApplications.ts b/keep-ui/app/(keep)/topology/model/useTopologyApplications.ts index 2bf46f2d3..7da395d93 100644 --- a/keep-ui/app/(keep)/topology/model/useTopologyApplications.ts +++ b/keep-ui/app/(keep)/topology/model/useTopologyApplications.ts @@ -2,10 +2,10 @@ import { TopologyApplication } from "./models"; import useSWR, { SWRConfiguration } from "swr"; import { useCallback, useMemo } from "react"; import { useTopology } from "./useTopology"; -import { useRevalidateMultiple } from "@/utils/state"; import { TOPOLOGY_URL } from "./useTopology"; import { KeepApiError } from "@/shared/api"; import { useApi } from "@/shared/lib/hooks/useApi"; +import { useRevalidateMultiple } from "@/shared/lib/state-utils"; type UseTopologyApplicationsOptions = { initialData?: TopologyApplication[]; diff --git a/keep-ui/components/navbar/AlertsLinks.tsx b/keep-ui/components/navbar/AlertsLinks.tsx index 01ed1e52f..8a856d42f 100644 --- a/keep-ui/components/navbar/AlertsLinks.tsx +++ b/keep-ui/components/navbar/AlertsLinks.tsx @@ -13,7 +13,7 @@ import CreatableMultiSelect from "@/components/ui/CreatableMultiSelect"; import { useLocalStorage } from "utils/hooks/useLocalStorage"; import { ActionMeta, MultiValue } from "react-select"; import { useTags } from "utils/hooks/useTags"; -import { usePresets } from "utils/hooks/usePresets"; +import { usePresets } from "@/entities/presets/model/usePresets"; import { useMounted } from "@/shared/lib/hooks/useMounted"; import clsx from "clsx"; @@ -34,9 +34,7 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { const { data: tags = [] } = useTags(); - // Get latest static presets (merged local and server presets) - const { useLatestStaticPresets } = usePresets(); - const { data: staticPresets = [] } = useLatestStaticPresets({ + const { staticPresets, error: staticPresetsError } = usePresets({ revalidateIfStale: true, revalidateOnFocus: true, }); @@ -61,11 +59,11 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { // Determine if we should show the feed link const shouldShowFeed = (() => { // For the initial render on the server, always show feed - if (!isMounted) { + if (!isMounted || (!staticPresets && !staticPresetsError)) { return true; } - return staticPresets.some((preset) => preset.name === "feed"); + return staticPresets?.some((preset) => preset.name === "feed"); })(); // Get the current alerts count only if we should show feed @@ -127,12 +125,7 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { )} - {session && isMounted && ( - - )} + )} diff --git a/keep-ui/components/navbar/CustomPresetAlertLinks.tsx b/keep-ui/components/navbar/CustomPresetAlertLinks.tsx index b2e9510f4..7ff6edde5 100644 --- a/keep-ui/components/navbar/CustomPresetAlertLinks.tsx +++ b/keep-ui/components/navbar/CustomPresetAlertLinks.tsx @@ -1,7 +1,5 @@ -import { CSSProperties, useEffect, useState } from "react"; -import { Session } from "next-auth"; -import { toast } from "react-toastify"; -import { usePresets } from "utils/hooks/usePresets"; +import { CSSProperties } from "react"; +import { usePresets } from "@/entities/presets/model/usePresets"; import { AiOutlineSwap } from "react-icons/ai"; import { usePathname, useRouter } from "next/navigation"; import { Subtitle } from "@tremor/react"; @@ -17,7 +15,6 @@ import { } from "@dnd-kit/core"; import { SortableContext, useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { Preset } from "@/app/(keep)/alerts/models"; import { AiOutlineSound } from "react-icons/ai"; // Using dynamic import to avoid hydration issues with react-player import dynamic from "next/dynamic"; @@ -25,15 +22,21 @@ const ReactPlayer = dynamic(() => import("react-player"), { ssr: false }); // import css import "./CustomPresetAlertLink.css"; import clsx from "clsx"; -import { useApi } from "@/shared/lib/hooks/useApi"; +import { Preset } from "@/entities/presets/model/types"; +import { usePresetActions } from "@/entities/presets/model/usePresetActions"; +import { usePresetPolling } from "@/entities/presets/model/usePresetPolling"; -type PresetAlertProps = { +type AlertPresetLinkProps = { preset: Preset; pathname: string | null; deletePreset: (id: string, name: string) => void; }; -const PresetAlert = ({ preset, pathname, deletePreset }: PresetAlertProps) => { +const AlertPresetLink = ({ + preset, + pathname, + deletePreset, +}: AlertPresetLinkProps) => { const href = `/alerts/${preset.name.toLowerCase()}`; const isActive = decodeURIComponent(pathname?.toLowerCase() || "") === href; @@ -90,72 +93,35 @@ const PresetAlert = ({ preset, pathname, deletePreset }: PresetAlertProps) => { ); }; type CustomPresetAlertLinksProps = { - session: Session; selectedTags: string[]; }; export const CustomPresetAlertLinks = ({ - session, selectedTags, }: CustomPresetAlertLinksProps) => { - const api = useApi(); + const { deletePreset } = usePresetActions(); - const { useAllPresets, presetsOrderFromLS, setPresetsOrderFromLS } = - usePresets(); - const { data: presets = [], mutate: presetsMutator } = useAllPresets({ + const { dynamicPresets: presets, setLocalDynamicPresets } = usePresets({ revalidateIfStale: false, revalidateOnFocus: false, }); + usePresetPolling(); + const pathname = usePathname(); const router = useRouter(); - const [presetsOrder, setPresetsOrder] = useState([]); // Check for noisy presets and control sound playback - const anyNoisyNow = presets.some((preset) => preset.should_do_noise_now); - - const checkValidPreset = (preset: Preset) => { - if (!preset.is_private) { - return true; - } - return preset && preset.created_by == session?.user?.email; - }; - - useEffect(() => { - const filteredLS = presetsOrderFromLS.filter( - (preset) => - ![ - "feed", - "deleted", - "dismissed", - "without-incident", - "groups", - ].includes(preset.name) - ); + const anyNoisyNow = presets?.some((preset) => preset.should_do_noise_now); - // Combine live presets and local storage order - const combinedOrder = presets.reduce( - (acc, preset: Preset) => { - if (!acc.find((p) => p.id === preset.id)) { - acc.push(preset); - } - return acc.filter((preset) => checkValidPreset(preset)); - }, - [...filteredLS] - ); - - // Only update state if there's an actual change to prevent infinite loops - if (JSON.stringify(presetsOrder) !== JSON.stringify(combinedOrder)) { - setPresetsOrder(combinedOrder); - } - }, [presets, presetsOrderFromLS]); // Filter presets based on tags, or return all if no tags are selected const filteredOrderedPresets = selectedTags.length === 0 - ? presetsOrder - : presetsOrder.filter((preset) => + ? presets + : presets.filter((preset) => preset.tags.some((tag) => selectedTags.includes(tag.name)) ); + const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { @@ -171,35 +137,10 @@ export const CustomPresetAlertLinks = ({ }) ); - const deletePreset = async (presetId: string, presetName: string) => { - const isDeleteConfirmed = confirm( - `You are about to delete preset ${presetName}. Are you sure?` - ); - - if (isDeleteConfirmed) { - try { - await api.delete(`/preset/${presetId}`); - - toast(`Preset ${presetName} deleted!`, { - position: "top-left", - type: "success", - }); - - await presetsMutator(); - - // remove preset from saved order - setPresetsOrderFromLS((oldOrder) => - oldOrder.filter((p) => p.id !== presetId) - ); - - router.push("/alerts/feed"); // Redirect to feed - } catch (error) { - toast(`Error deleting preset ${presetName}: ${error}`, { - position: "top-left", - type: "error", - }); - } - } + const deletePresetAndRedirect = (presetId: string, presetName: string) => { + deletePreset(presetId, presetName).then(() => { + router.push("/alerts/feed"); + }); }; const onDragEnd = (event: DragEndEvent) => { @@ -209,22 +150,20 @@ export const CustomPresetAlertLinks = ({ return; } - const fromIndex = presetsOrder.findIndex( + const fromIndex = presets.findIndex( ({ id }) => id === active.id.toString() ); - const toIndex = presetsOrder.findIndex( - ({ id }) => id === over.id.toString() - ); + const toIndex = presets.findIndex(({ id }) => id === over.id.toString()); if (toIndex === -1) { return; } - const reorderedCols = [...presetsOrder]; + const reorderedCols = [...presets]; const reorderedItem = reorderedCols.splice(fromIndex, 1); reorderedCols.splice(toIndex, 0, reorderedItem[0]); - setPresetsOrderFromLS(reorderedCols); + setLocalDynamicPresets(reorderedCols); }; return ( @@ -234,13 +173,13 @@ export const CustomPresetAlertLinks = ({ collisionDetection={rectIntersection} onDragEnd={onDragEnd} > - + {filteredOrderedPresets.map((preset) => ( - ))} diff --git a/keep-ui/components/navbar/UserInfo.tsx b/keep-ui/components/navbar/UserInfo.tsx index bc8ad7c9d..64d2657c2 100644 --- a/keep-ui/components/navbar/UserInfo.tsx +++ b/keep-ui/components/navbar/UserInfo.tsx @@ -18,6 +18,7 @@ import { useSignOut } from "@/shared/lib/hooks/useSignOut"; import { FaSlack } from "react-icons/fa"; import { ThemeControl } from "@/shared/ui/theme/ThemeControl"; import { HiOutlineDocumentText } from "react-icons/hi2"; +import { useMounted } from "@/shared/lib/hooks/useMounted"; const ONBOARDING_FLOW_ID = "flow_FHDz1hit"; @@ -88,11 +89,12 @@ type UserInfoProps = { export const UserInfo = ({ session }: UserInfoProps) => { const { flow } = Frigade.useFlow(ONBOARDING_FLOW_ID); const [isOnboardingOpen, setIsOnboardingOpen] = useState(false); + const isMounted = useMounted(); return ( <>