diff --git a/keep-ui/app/(keep)/ai/ai.tsx b/keep-ui/app/(keep)/ai/ai.tsx index 2446337a7..a4f75482c 100644 --- a/keep-ui/app/(keep)/ai/ai.tsx +++ b/keep-ui/app/(keep)/ai/ai.tsx @@ -2,10 +2,11 @@ import { Card, List, ListItem, Title, Subtitle } from "@tremor/react"; import { useAIStats, usePollAILogs } from "utils/hooks/useAI"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { toast } from "react-toastify"; import { useEffect, useState, useRef, FormEvent } from "react"; import { AILogs } from "./model"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; export default function Ai() { const { data: aistats, isLoading } = useAIStats(); @@ -16,6 +17,7 @@ export default function Ai() { const [animate, setAnimate] = useState(false); const onlyOnce = useRef(false); const apiUrl = useApiUrl(); + const configData = useConfig(); const mutateAILogs = (logs: AILogs) => { setBasicAlgorithmLog(logs.log); @@ -52,8 +54,8 @@ export default function Ai() { body: JSON.stringify({}), }); if (!response.ok) { - toast.error( - "Failed to mine incidents, please contact us if this issue persists." + ReadOnlyAwareToaster.error( + "Failed to mine incidents, please contact us if this issue persists.", configData ); } diff --git a/keep-ui/app/(keep)/alerts/ViewAlertModal.tsx b/keep-ui/app/(keep)/alerts/ViewAlertModal.tsx index 7bc404172..22d2e4478 100644 --- a/keep-ui/app/(keep)/alerts/ViewAlertModal.tsx +++ b/keep-ui/app/(keep)/alerts/ViewAlertModal.tsx @@ -2,11 +2,12 @@ import { AlertDto } from "./models"; // Adjust the import path as needed import Modal from "@/components/ui/Modal"; // Ensure this path matches your project structure import { Button, Icon, Switch, Text } from "@tremor/react"; import { toast } from "react-toastify"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { XMarkIcon } from "@heroicons/react/24/outline"; import "./ViewAlertModal.css"; import React, { useState } from "react"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; interface ViewAlertModalProps { alert: AlertDto | null | undefined; @@ -27,6 +28,7 @@ export const ViewAlertModal: React.FC = ({ const [showHighlightedOnly, setShowHighlightedOnly] = useState(false); const { data: session } = useSession(); const apiUrl = useApiUrl(); + const { data: configData } = useConfig(); const unEnrichAlert = async (key: string) => { if (confirm(`Are you sure you want to un-enrich ${key}?`)) { @@ -49,12 +51,12 @@ export const ViewAlertModal: React.FC = ({ await mutate(); } else { // Handle error - toast.error(`Failed to un-enriched ${key}`); + ReadOnlyAwareToaster.error(`Failed to un-enriched ${key}`, configData); await mutate(); } } catch (error) { // Handle unexpected error - toast.error("An unexpected error occurred"); + ReadOnlyAwareToaster.error("An unexpected error occurred", configData); } } }; @@ -104,7 +106,7 @@ export const ViewAlertModal: React.FC = ({ await navigator.clipboard.writeText(JSON.stringify(alert, null, 2)); toast.success("Alert copied to clipboard!"); } catch (err) { - toast.error("Failed to copy alert."); + ReadOnlyAwareToaster.error("Failed to copy alert.", configData); } } }; diff --git a/keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx b/keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx index ff8de5ab5..2af060496 100644 --- a/keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx @@ -5,7 +5,7 @@ import { CreateOrUpdateIncidentForm } from "@/features/create-or-update-incident import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { FormEvent, useCallback, useEffect, useState } from "react"; import { toast } from "react-toastify"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { useIncidents, usePollIncidents, @@ -13,6 +13,7 @@ import { import Loading from "@/app/(keep)/loading"; import { AlertDto } from "./models"; import { getIncidentName } from "@/entities/incidents/lib/utils"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; interface AlertAssociateIncidentModalProps { isOpen: boolean; @@ -28,6 +29,7 @@ const AlertAssociateIncidentModal = ({ alerts, }: AlertAssociateIncidentModalProps) => { const [createIncident, setCreateIncident] = useState(false); + const { data: configData} = useConfig(); const { data: incidents, isLoading, mutate } = useIncidents(true, 100); usePollIncidents(mutate); @@ -54,8 +56,8 @@ const AlertAssociateIncidentModal = ({ await mutate(); toast.success("Alerts associated with incident successfully"); } else { - toast.error( - "Failed to associated alerts with incident, please contact us if this issue persists." + ReadOnlyAwareToaster.error( + "Failed to associated alerts with incident, please contact us if this issue persists.", configData ); } }, 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 f68f46e61..64386dc1a 100644 --- a/keep-ui/app/(keep)/alerts/alert-change-status-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-change-status-modal.tsx @@ -8,7 +8,7 @@ import Select, { } from "react-select"; import { useState } from "react"; import { AlertDto, Status } from "./models"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { toast } from "react-toastify"; import { @@ -20,6 +20,7 @@ import { } from "@heroicons/react/24/outline"; import { usePresets } from "utils/hooks/usePresets"; import { useAlerts } from "utils/hooks/useAlerts"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; const statusIcons = { [Status.Firing]: , @@ -75,6 +76,7 @@ export default function AlertChangeStatusModal({ presetName, }: Props) { const { data: session } = useSession(); + const { data: configData } = useConfig(); const [selectedStatus, setSelectedStatus] = useState(null); const { useAllPresets } = usePresets(); const { mutate: presetsMutator } = useAllPresets(); @@ -105,7 +107,7 @@ export default function AlertChangeStatusModal({ const handleChangeStatus = async () => { if (!selectedStatus) { - toast.error("Please select a new status."); + ReadOnlyAwareToaster.error("Please select a new status.", configData); return; } @@ -137,10 +139,10 @@ export default function AlertChangeStatusModal({ await alertsMutator(); await presetsMutator(); } else { - toast.error("Failed to change alert status."); + ReadOnlyAwareToaster.error("Failed to change alert status.", configData); } } catch (error) { - toast.error("An error occurred while changing alert status."); + ReadOnlyAwareToaster.error("An error occurred while changing alert status.", configData); } }; diff --git a/keep-ui/app/(keep)/alerts/alert-method-modal.tsx b/keep-ui/app/(keep)/alerts/alert-method-modal.tsx index 5bd4bfdb2..ce99d15c1 100644 --- a/keep-ui/app/(keep)/alerts/alert-method-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-method-modal.tsx @@ -6,7 +6,7 @@ import { ProviderMethodParam, } from "@/app/(keep)/providers/providers"; import { getSession } from "next-auth/react"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { toast } from "react-toastify"; import Loading from "@/app/(keep)/loading"; import { @@ -22,6 +22,7 @@ import { useAlerts } from "utils/hooks/useAlerts"; import { useRouter, useSearchParams } from "next/navigation"; import { useProviders } from "utils/hooks/useProviders"; import Modal from "@/components/ui/Modal"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; const supportedParamTypes = ["datetime", "literal", "str"]; @@ -33,6 +34,7 @@ export function AlertMethodModal({ presetName }: AlertMethodModalProps) { const searchParams = useSearchParams(); const router = useRouter(); const apiUrl = useApiUrl(); + const { data: configData} = useConfig(); const alertFingerprint = searchParams?.get("alertFingerprint"); const providerId = searchParams?.get("providerId"); @@ -187,18 +189,19 @@ export function AlertMethodModal({ presetName }: AlertMethodModalProps) { setIsLoading(false); } } else { - toast.error( + ReadOnlyAwareToaster.error( `Failed to invoke "${method.name}" on ${ provider.details.name ?? provider.id - } due to ${responseObject.detail}`, + } due to ${responseObject.detail}`, configData, { position: toast.POSITION.TOP_LEFT } ); } } catch (e: any) { - toast.error( + ReadOnlyAwareToaster.error( `Failed to invoke "${method.name}" on ${ provider.details.name ?? provider.id - } due to ${e.message}`, + } due to ${e.message}`, + configData, { position: toast.POSITION.TOP_LEFT } ); handleClose(); diff --git a/keep-ui/app/(keep)/alerts/alert-run-workflow-modal.tsx b/keep-ui/app/(keep)/alerts/alert-run-workflow-modal.tsx index 514719fb4..74862261c 100644 --- a/keep-ui/app/(keep)/alerts/alert-run-workflow-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-run-workflow-modal.tsx @@ -4,9 +4,10 @@ import Modal from "@/components/ui/Modal"; import { useWorkflows } from "utils/hooks/useWorkflows"; import { useState } from "react"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { toast } from "react-toastify"; import { useRouter } from "next/navigation"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; interface Props { alert: AlertDto | null | undefined; @@ -24,6 +25,7 @@ export default function AlertRunWorkflowModal({ alert, handleClose }: Props) { const { data: session } = useSession(); const router = useRouter(); const apiUrl = useApiUrl(); + const { data: configData } = useConfig(); const isOpen = !!alert; @@ -54,7 +56,7 @@ export default function AlertRunWorkflowModal({ alert, handleClose }: Props) { `/workflows/${selectedWorkflowId}/runs/${workflow_execution_id}` ); } else { - toast.error("Failed to start workflow", { position: "top-left" }); + ReadOnlyAwareToaster.error("Failed to start workflow", configData, { position: "top-left" }); } clearAndClose(); }; diff --git a/keep-ui/app/(keep)/extraction/create-or-update-extraction-rule.tsx b/keep-ui/app/(keep)/extraction/create-or-update-extraction-rule.tsx index ca50f4b4d..65f1f48cf 100644 --- a/keep-ui/app/(keep)/extraction/create-or-update-extraction-rule.tsx +++ b/keep-ui/app/(keep)/extraction/create-or-update-extraction-rule.tsx @@ -16,11 +16,12 @@ import { import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { FormEvent, useEffect, useState } from "react"; import { toast } from "react-toastify"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { ExtractionRule } from "./model"; import { extractNamedGroups } from "./extractions-table"; import { useExtractions } from "utils/hooks/useExtractionRules"; import { AlertsRulesBuilder } from "@/app/(keep)/alerts/alerts-rules-builder"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; interface Props { extractionToEdit: ExtractionRule | null; @@ -32,6 +33,7 @@ export default function CreateOrUpdateExtractionRule({ editCallback, }: Props) { const { data: session } = useSession(); + const { data: configData } = useConfig(); const { mutate } = useExtractions(); const [extractionName, setExtractionName] = useState(""); const [isPreFormatting, setIsPreFormatting] = useState(false); @@ -97,8 +99,8 @@ export default function CreateOrUpdateExtractionRule({ mutate(); toast.success("Extraction rule created successfully"); } else { - toast.error( - "Failed to create extraction rule, please contact us if this issue persists." + ReadOnlyAwareToaster.error( + "Failed to create extraction rule, please contact us if this issue persists.", configData ); } }; @@ -130,8 +132,8 @@ export default function CreateOrUpdateExtractionRule({ mutate(); toast.success("Extraction updated successfully"); } else { - toast.error( - "Failed to update extraction, please contact us if this issue persists." + ReadOnlyAwareToaster.error( + "Failed to update extraction, please contact us if this issue persists.", configData ); } }; diff --git a/keep-ui/app/(keep)/extraction/extractions-table.tsx b/keep-ui/app/(keep)/extraction/extractions-table.tsx index 7b621b05b..c333b97ee 100644 --- a/keep-ui/app/(keep)/extraction/extractions-table.tsx +++ b/keep-ui/app/(keep)/extraction/extractions-table.tsx @@ -19,7 +19,7 @@ import { } from "@tanstack/react-table"; import { MdRemoveCircle, MdModeEdit } from "react-icons/md"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { useMappings } from "utils/hooks/useMappingRules"; import { toast } from "react-toastify"; import { ExtractionRule } from "./model"; @@ -27,6 +27,7 @@ import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"; import { IoCheckmark } from "react-icons/io5"; import { HiMiniXMark } from "react-icons/hi2"; import { useState } from "react"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; const columnHelper = createColumnHelper(); @@ -53,6 +54,7 @@ export default function RulesTable({ }: Props) { const { data: session } = useSession(); const { mutate } = useMappings(); + const { data: configData } = useConfig(); const apiUrl = useApiUrl(); const [expanded, setExpanded] = useState({}); @@ -193,8 +195,8 @@ export default function RulesTable({ mutate(); toast.success("Extraction deleted successfully"); } else { - toast.error( - "Failed to delete extraction rule, contact us if this persists" + ReadOnlyAwareToaster.error( + "Failed to delete extraction rule, contact us if this persists", configData ); } }); diff --git a/keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityComment.tsx b/keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityComment.tsx index d015b02e9..a027453df 100644 --- a/keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityComment.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityComment.tsx @@ -1,11 +1,12 @@ import { IncidentDto } from "@/entities/incidents/model"; import { AuditEvent } from "@/utils/hooks/useAlerts"; -import { useApiUrl } from "@/utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "@/utils/hooks/useConfig"; import { TextInput, Button } from "@tremor/react"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { useState, useCallback, useEffect } from "react"; import { toast } from "react-toastify"; import { KeyedMutator } from "swr"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; export function IncidentActivityComment({ incident, @@ -17,6 +18,7 @@ export function IncidentActivityComment({ const [comment, setComment] = useState(""); const apiUrl = useApiUrl(); const { data: session } = useSession(); + const { data: configData } = useConfig(); const onSubmit = useCallback(async () => { const response = await fetch(`${apiUrl}/incidents/${incident.id}/comment`, { @@ -35,7 +37,7 @@ export function IncidentActivityComment({ setComment(""); mutator(); } else { - toast.error("Failed to add comment", { position: "top-right" }); + ReadOnlyAwareToaster.error("Failed to add comment", configData, { position: "top-right" }); } }, [ apiUrl, diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx index 799afbc28..60b3b2f01 100644 --- a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx @@ -2,9 +2,10 @@ import { Icon } from "@tremor/react"; import { AlertDto } from "@/app/(keep)/alerts/models"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { toast } from "react-toastify"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { useIncidentAlerts } from "utils/hooks/useIncidents"; import { LinkSlashIcon } from "@heroicons/react/24/outline"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; interface Props { incidentId: string; @@ -14,6 +15,7 @@ export default function IncidentAlertMenu({ incidentId, alert }: Props) { const apiUrl = useApiUrl(); const { data: session } = useSession(); const { mutate } = useIncidentAlerts(incidentId); + const { data: configData } = useConfig(); function onRemove() { if (confirm("Are you sure you want to remove correlation?")) { @@ -31,8 +33,9 @@ export default function IncidentAlertMenu({ incidentId, alert }: Props) { }); mutate(); } else { - toast.error( + ReadOnlyAwareToaster.error( "Failed to remove alert from incident, please contact us if this issue persists.", + configData, { position: "top-right", } diff --git a/keep-ui/app/(keep)/maintenance/create-or-update-maintenance-rule.tsx b/keep-ui/app/(keep)/maintenance/create-or-update-maintenance-rule.tsx index 6c46152cf..8557633ad 100644 --- a/keep-ui/app/(keep)/maintenance/create-or-update-maintenance-rule.tsx +++ b/keep-ui/app/(keep)/maintenance/create-or-update-maintenance-rule.tsx @@ -13,13 +13,14 @@ import { import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { FormEvent, useEffect, useState } from "react"; import { toast } from "react-toastify"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { MaintenanceRule } from "./model"; import { useMaintenanceRules } from "utils/hooks/useMaintenanceRules"; import { AlertsRulesBuilder } from "@/app/(keep)/alerts/alerts-rules-builder"; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; import { useRouter } from "next/navigation"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; interface Props { maintenanceToEdit: MaintenanceRule | null; @@ -43,6 +44,8 @@ export default function CreateOrUpdateMaintenanceRule({ const editMode = maintenanceToEdit !== null; const router = useRouter(); const apiUrl = useApiUrl(); + const { data: configData } = useConfig(); + useEffect(() => { if (maintenanceToEdit) { setMaintenanceName(maintenanceToEdit.name); @@ -109,8 +112,9 @@ export default function CreateOrUpdateMaintenanceRule({ mutate(); toast.success("Maintenance rule created successfully"); } else { - toast.error( - "Failed to create maintenance rule, please contact us if this issue persists." + ReadOnlyAwareToaster.error( + "Failed to create maintenance rule, please contact us if this issue persists.", + configData ); } }; @@ -140,8 +144,8 @@ export default function CreateOrUpdateMaintenanceRule({ mutate(); toast.success("Maintenance rule updated successfully"); } else { - toast.error( - "Failed to update maintenance rule, please contact us if this issue persists." + ReadOnlyAwareToaster.error( + "Failed to update maintenance rule, please contact us if this issue persists.", configData ); } }; diff --git a/keep-ui/app/(keep)/maintenance/maintenance-rules-table.tsx b/keep-ui/app/(keep)/maintenance/maintenance-rules-table.tsx index 790082e83..1aad2f7cf 100644 --- a/keep-ui/app/(keep)/maintenance/maintenance-rules-table.tsx +++ b/keep-ui/app/(keep)/maintenance/maintenance-rules-table.tsx @@ -19,11 +19,12 @@ import { import { MdRemoveCircle, MdModeEdit } from "react-icons/md"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { toast } from "react-toastify"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { MaintenanceRule } from "./model"; import { IoCheckmark } from "react-icons/io5"; import { HiMiniXMark } from "react-icons/hi2"; import { useState } from "react"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; const columnHelper = createColumnHelper(); @@ -37,6 +38,7 @@ export default function MaintenanceRulesTable({ editCallback, }: Props) { const { data: session } = useSession(); + const { data: configData } = useConfig(); const apiUrl = useApiUrl(); const [expanded, setExpanded] = useState({}); @@ -133,8 +135,9 @@ export default function MaintenanceRulesTable({ if (response.ok) { toast.success("Maintenance rule deleted successfully"); } else { - toast.error( - "Failed to delete maintenance rule, contact us if this persists" + ReadOnlyAwareToaster.error( + "Failed to delete maintenance rule, contact us if this persists", + configData ); } }); diff --git a/keep-ui/app/(keep)/mapping/create-or-edit-mapping.tsx b/keep-ui/app/(keep)/mapping/create-or-edit-mapping.tsx index c526d49bd..94e741e26 100644 --- a/keep-ui/app/(keep)/mapping/create-or-edit-mapping.tsx +++ b/keep-ui/app/(keep)/mapping/create-or-edit-mapping.tsx @@ -28,11 +28,12 @@ import { } from "react"; import { usePapaParse } from "react-papaparse"; import { toast } from "react-toastify"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { useMappings } from "utils/hooks/useMappingRules"; import { MappingRule } from "./models"; import { CreateableSearchSelect } from "@/components/ui/CreateableSearchSelect"; import { useTopology } from "@/app/(keep)/topology/model"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; interface Props { editRule: MappingRule | null; @@ -41,6 +42,7 @@ interface Props { export default function CreateOrEditMapping({ editRule, editCallback }: Props) { const { data: session } = useSession(); + const { data: configData } = useConfig(); const { mutate } = useMappings(); const [tabIndex, setTabIndex] = useState(0); const [mapName, setMapName] = useState(""); @@ -155,8 +157,9 @@ export default function CreateOrEditMapping({ editRule, editCallback }: Props) { mutate(); toast.success("Mapping created successfully"); } else { - toast.error( - "Failed to create mapping, please contact us if this issue persists." + ReadOnlyAwareToaster.error( + "Failed to create mapping, please contact us if this issue persists.", + configData ); } }; @@ -186,8 +189,9 @@ export default function CreateOrEditMapping({ editRule, editCallback }: Props) { mutate(); toast.success("Mapping updated successfully"); } else { - toast.error( - "Failed to update mapping, please contact us if this issue persists." + ReadOnlyAwareToaster.error( + "Failed to update mapping, please contact us if this issue persists.", + configData ); } }; diff --git a/keep-ui/app/(keep)/mapping/rules-table.tsx b/keep-ui/app/(keep)/mapping/rules-table.tsx index dc80a5326..1efe71bc3 100644 --- a/keep-ui/app/(keep)/mapping/rules-table.tsx +++ b/keep-ui/app/(keep)/mapping/rules-table.tsx @@ -19,10 +19,11 @@ import { } from "@tanstack/react-table"; import { MdRemoveCircle, MdModeEdit } from "react-icons/md"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { useMappings } from "utils/hooks/useMappingRules"; import { toast } from "react-toastify"; import { useState } from "react"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; const columnHelper = createColumnHelper(); @@ -33,6 +34,7 @@ interface Props { export default function RulesTable({ mappings, editCallback }: Props) { const { data: session } = useSession(); + const { data: configData } = useConfig(); const { mutate } = useMappings(); const apiUrl = useApiUrl(); const [expanded, setExpanded] = useState({}); @@ -126,7 +128,10 @@ export default function RulesTable({ mappings, editCallback }: Props) { mutate(); toast.success("Rule deleted successfully"); } else { - toast.error("Failed to delete rule, contact us if this persists"); + ReadOnlyAwareToaster.error( + "Failed to delete rule, contact us if this persists", + configData, + ); } }); } diff --git a/keep-ui/app/(keep)/providers/page.client.tsx b/keep-ui/app/(keep)/providers/page.client.tsx index b4e4131ad..3ab82a84d 100644 --- a/keep-ui/app/(keep)/providers/page.client.tsx +++ b/keep-ui/app/(keep)/providers/page.client.tsx @@ -2,7 +2,7 @@ import { defaultProvider, Provider } from "./providers"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { KeepApiError } from "@/shared/lib/KeepApiError"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import ProvidersTiles from "./providers-tiles"; import React, { useState, useEffect } from "react"; import Loading from "@/app/(keep)/loading"; @@ -10,6 +10,7 @@ import { useFilterContext } from "./filter-context"; import { toast } from "react-toastify"; import { useRouter } from "next/navigation"; import { useProviders } from "@/utils/hooks/useProviders"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; export const useFetchProviders = () => { const [providers, setProviders] = useState([]); @@ -117,12 +118,15 @@ export default function ProvidersPage({ } = useFilterContext(); const apiUrl = useApiUrl(); const router = useRouter(); + const { data: configData } = useConfig(); + useEffect(() => { if (searchParams?.oauth === "failure") { const reason = JSON.parse(searchParams.reason); - toast.error(`Failed to install provider: ${reason.detail}`, { - position: toast.POSITION.TOP_LEFT, - }); + ReadOnlyAwareToaster.error(`Failed to install provider: ${reason.detail}`, + configData, + {position: toast.POSITION.TOP_LEFT,} + ); } else if (searchParams?.oauth === "success") { toast.success("Successfully installed provider", { position: toast.POSITION.TOP_LEFT, diff --git a/keep-ui/app/(keep)/providers/provider-form.tsx b/keep-ui/app/(keep)/providers/provider-form.tsx index 7e58e8a25..542b3d2c2 100644 --- a/keep-ui/app/(keep)/providers/provider-form.tsx +++ b/keep-ui/app/(keep)/providers/provider-form.tsx @@ -53,6 +53,7 @@ import "./provider-form.css"; import { useProviders } from "@/utils/hooks/useProviders"; import TimeAgo from "react-timeago"; import { toast } from "react-toastify"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; type ProviderFormProps = { provider: Provider; @@ -150,6 +151,7 @@ const ProviderForm = ({ }: ProviderFormProps) => { console.log("Loading the ProviderForm component"); const { mutate } = useProviders(); + const { data: configData } = useConfig(); const searchParams = useSearchParams(); const [activeTabsState, setActiveTabsState] = useState({}); const initialData = { @@ -292,7 +294,7 @@ const ProviderForm = ({ mutate(); closeModal(); } else { - toast.error(`Failed to delete ${provider.type} 😢`); + ReadOnlyAwareToaster.error(`Failed to delete ${provider.type} 😢`, configData); } } } @@ -452,7 +454,7 @@ const ProviderForm = ({ mutate(); }) .catch((error) => { - toast.error("Failed to update provider", { position: "top-left" }); + ReadOnlyAwareToaster.error("Failed to update provider", configData, { position: "top-left" }); const updatedFormErrors = error.toString(); setFormErrors(updatedFormErrors); onFormChange(formValues, updatedFormErrors); diff --git a/keep-ui/app/(keep)/topology/ui/applications/applications-list.tsx b/keep-ui/app/(keep)/topology/ui/applications/applications-list.tsx index b1eaaa320..180693d2e 100644 --- a/keep-ui/app/(keep)/topology/ui/applications/applications-list.tsx +++ b/keep-ui/app/(keep)/topology/ui/applications/applications-list.tsx @@ -14,6 +14,8 @@ import { useTopologySearchContext, } from "../../TopologySearchContext"; import { ApplicationModal } from "@/app/(keep)/topology/ui/applications/application-modal"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; +import { useConfig } from "@/utils/hooks/useConfig"; type ModalState = { isOpen: boolean; @@ -32,6 +34,7 @@ export function ApplicationsList({ }: { applications?: TopologyApplication[]; }) { + const { data: configData } = useConfig(); const { applications, addApplication, removeApplication, updateApplication } = useTopologyApplications({ initialData: initialApplications, @@ -66,7 +69,7 @@ export function ApplicationsList({ updateApplication(updatedApplication).then( () => {}, (error) => { - toast.error("Failed to update application"); + ReadOnlyAwareToaster.error("Failed to update application", configData); } ); }, @@ -79,7 +82,7 @@ export function ApplicationsList({ removeApplication(applicationId); setModalState(initialModalState); } catch (error) { - toast.error("Failed to delete application"); + ReadOnlyAwareToaster.error("Failed to delete application", configData); } }, [removeApplication] diff --git a/keep-ui/app/(keep)/topology/ui/map/manage-selection.tsx b/keep-ui/app/(keep)/topology/ui/map/manage-selection.tsx index b4cc81653..e047ff4e0 100644 --- a/keep-ui/app/(keep)/topology/ui/map/manage-selection.tsx +++ b/keep-ui/app/(keep)/topology/ui/map/manage-selection.tsx @@ -14,8 +14,11 @@ import { import { toast } from "react-toastify"; import { TopologySearchContext } from "../../TopologySearchContext"; import { ApplicationModal } from "@/app/(keep)/topology/ui/applications/application-modal"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; +import { useConfig } from "@/utils/hooks/useConfig"; export function ManageSelection({ className }: { className?: string }) { + const { data: configData } = useConfig(); const { setSelectedObjectId } = useContext(TopologySearchContext); const { applications, addApplication, removeApplication, updateApplication } = useTopologyApplications(); @@ -81,7 +84,7 @@ export function ManageSelection({ className }: { className?: string }) { setSelectedObjectId(updatedApplication.id); }, (error) => { - toast.error("Failed to update application"); + ReadOnlyAwareToaster.error("Failed to update application", configData); } ); }; @@ -103,7 +106,7 @@ export function ManageSelection({ className }: { className?: string }) { setSelectedApplication(null); setIsModalOpen(false); } catch (error) { - toast.error("Failed to delete application"); + ReadOnlyAwareToaster.error("Failed to delete application", configData); } }, [removeApplication] diff --git a/keep-ui/app/(keep)/workflows/manual-run-workflow-modal.tsx b/keep-ui/app/(keep)/workflows/manual-run-workflow-modal.tsx index eb4717c94..a498dd496 100644 --- a/keep-ui/app/(keep)/workflows/manual-run-workflow-modal.tsx +++ b/keep-ui/app/(keep)/workflows/manual-run-workflow-modal.tsx @@ -4,11 +4,12 @@ import Modal from "@/components/ui/Modal"; import { useWorkflows } from "utils/hooks/useWorkflows"; import { useState } from "react"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; -import { useApiUrl } from "utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "utils/hooks/useConfig"; import { toast } from "react-toastify"; import { useRouter } from "next/navigation"; import { IncidentDto } from "@/entities/incidents/model"; import { AlertDto } from "@/app/(keep)/alerts/models"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; interface Props { alert?: AlertDto | null | undefined; @@ -29,6 +30,7 @@ export default function ManualRunWorkflowModal({ >(undefined); const { data: workflows } = useWorkflows({}); const { data: session } = useSession(); + const { data: configData } = useConfig(); const router = useRouter(); const apiUrl = useApiUrl(); @@ -64,7 +66,7 @@ export default function ManualRunWorkflowModal({ `/workflows/${selectedWorkflowId}/runs/${workflow_execution_id}` ); } else { - toast.error("Failed to start workflow", { position: "top-left" }); + ReadOnlyAwareToaster.error("Failed to start workflow", configData, { position: "top-left" }); } clearAndClose(); }; diff --git a/keep-ui/entities/incidents/model/useIncidentActions.tsx b/keep-ui/entities/incidents/model/useIncidentActions.tsx index 76b3074ad..2f9c15f7d 100644 --- a/keep-ui/entities/incidents/model/useIncidentActions.tsx +++ b/keep-ui/entities/incidents/model/useIncidentActions.tsx @@ -1,9 +1,10 @@ -import { useApiUrl } from "@/utils/hooks/useConfig"; +import { useApiUrl, useConfig } from "@/utils/hooks/useConfig"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { useCallback } from "react"; -import { toast } from "react-toastify"; import { useSWRConfig } from "swr"; import { IncidentDto, Status } from "./models"; +import { ReadOnlyAwareToaster } from "@/shared/lib/ReadOnlyAwareToaster"; +import { toast } from "react-toastify"; type UseIncidentActionsValue = { addIncident: (incident: IncidentCreateDto) => Promise; @@ -45,6 +46,7 @@ export function useIncidentActions(): UseIncidentActionsValue { const apiUrl = useApiUrl(); const { data: session } = useSession(); const { mutate } = useSWRConfig(); + const { data: configData } = useConfig(); const mutateIncidentsList = useCallback( () => @@ -76,8 +78,8 @@ export function useIncidentActions(): UseIncidentActionsValue { toast.success("Incident created successfully"); return await response.json(); } else { - toast.error( - "Failed to create incident, please contact us if this issue persists." + ReadOnlyAwareToaster.error( + "Failed to create incident, please contact us if this issue persists.", configData ); } }, @@ -106,8 +108,8 @@ export function useIncidentActions(): UseIncidentActionsValue { mutateIncident(incidentId); toast.success("Incident updated successfully"); } else { - toast.error( - "Failed to update incident, please contact us if this issue persists." + ReadOnlyAwareToaster.error( + "Failed to update incident, please contact us if this issue persists.", configData ); } }, @@ -120,7 +122,7 @@ export function useIncidentActions(): UseIncidentActionsValue { destinationIncident: IncidentDto ) => { if (!sourceIncidents.length || !destinationIncident) { - toast.error("Please select incidents to merge."); + ReadOnlyAwareToaster.error("Please select incidents to merge.", configData); return; } @@ -141,10 +143,10 @@ export function useIncidentActions(): UseIncidentActionsValue { toast.success("Incidents merged successfully!"); mutateIncidentsList(); } else { - toast.error("Failed to merge incidents."); + ReadOnlyAwareToaster.error("Failed to merge incidents.", configData); } } catch (error) { - toast.error("An error occurred while merging incidents."); + ReadOnlyAwareToaster.error("An error occurred while merging incidents.", configData); } }, [apiUrl, mutateIncidentsList, session?.accessToken] @@ -170,7 +172,7 @@ export function useIncidentActions(): UseIncidentActionsValue { toast.success("Incident deleted successfully"); return true; } else { - toast.error("Failed to delete incident, contact us if this persists"); + ReadOnlyAwareToaster.error("Failed to delete incident, contact us if this persists", configData); return false; } }, @@ -180,7 +182,7 @@ export function useIncidentActions(): UseIncidentActionsValue { const changeStatus = useCallback( async (incidentId: string, status: Status, comment?: string) => { if (!status) { - toast.error("Please select a new status."); + ReadOnlyAwareToaster.error("Please select a new status.", configData); return; } @@ -204,10 +206,10 @@ export function useIncidentActions(): UseIncidentActionsValue { toast.success("Incident status changed successfully!"); mutateIncidentsList(); } else { - toast.error("Failed to change incident status."); + ReadOnlyAwareToaster.error("Failed to change incident status.", configData); } } catch (error) { - toast.error("An error occurred while changing incident status."); + ReadOnlyAwareToaster.error("An error occurred while changing incident status.", configData); } }, [apiUrl, mutateIncidentsList, session?.accessToken] @@ -231,8 +233,9 @@ export function useIncidentActions(): UseIncidentActionsValue { mutateIncident(incidentId); toast.success("Predicted incident confirmed successfully"); } else { - toast.error( - "Failed to confirm predicted incident, please contact us if this issue persists." + ReadOnlyAwareToaster.error( + "Failed to confirm predicted incident, please contact us if this issue persists.", + configData ); } }, diff --git a/keep-ui/shared/lib/ReadOnlyAwareToaster.ts b/keep-ui/shared/lib/ReadOnlyAwareToaster.ts new file mode 100644 index 000000000..c213c72bf --- /dev/null +++ b/keep-ui/shared/lib/ReadOnlyAwareToaster.ts @@ -0,0 +1,15 @@ +import { toast } from "react-toastify"; + +interface configData { + READ_ONLY: boolean; +} + +export class ReadOnlyAwareToaster { + static error(message: string, config: configData | null, toast_args: any = {}) { + if (config?.READ_ONLY) { + toast.warning("Changes are disabled in read-only mode. Sign up at keephq.dev to get your own instance!") + } else { + toast.error(message, toast_args) + } + } +} \ No newline at end of file