diff --git a/client/src/Pages/Incidents2/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents2/Components/IncidentDetailsModal/index.jsx new file mode 100644 index 000000000..25af835b9 --- /dev/null +++ b/client/src/Pages/Incidents2/Components/IncidentDetailsModal/index.jsx @@ -0,0 +1,406 @@ +// Components +import { Stack, Typography, Grid, Box, Button, Divider } from "@mui/material"; +import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; +import SummaryCard from "@/Pages/Incidents2/Components/SummaryCard"; +import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; + +// Utils +import { useEffect, useState } from "react"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { formatDateWithTz } from "@/Utils/timeUtils"; +import { useSelector } from "react-redux"; +import PropTypes from "prop-types"; + +// Hooks +import useFetchIncidents from "@/Pages/Incidents2/hooks/useFetchIncidents"; +import useGetIncidentsDuration from "@/Pages/Incidents2/hooks/useGetIncidentsDuration"; + +const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { fetchIncidentById, resolveIncident } = useFetchIncidents(); + const [incident, setIncident] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isResolveDialogOpen, setIsResolveDialogOpen] = useState(false); + const uiTimezone = useSelector((state) => state.ui.timezone); + const isActive = incident?.status === true; + const duration = useGetIncidentsDuration(incident || null, isActive); + + const statusColor = isActive ? theme.palette.error.main : theme.palette.success.main; + const toCapitalLetter = (text) => + typeof text === "string" && text.length > 0 + ? text.charAt(0).toUpperCase() + text.slice(1) + : text; + + const KeyValueRow = ({ label, value, sx = {} }) => ( + + + {label} + + + + {value ?? "-"} + + + + ); + + useEffect(() => { + const loadIncident = async () => { + if (!incidentId || !open) { + setIncident(null); + setIsLoading(false); + return; + } + + setIsLoading(true); + try { + const incidentData = await fetchIncidentById(incidentId); + setIncident(incidentData); + } catch (error) { + console.error("Error fetching incident:", error); + setIncident(null); + } finally { + setIsLoading(false); + } + }; + + loadIncident(); + }, [incidentId, open, fetchIncidentById]); + + const openResolveDialog = () => { + setIsResolveDialogOpen(true); + }; + + const closeResolveDialog = () => { + setIsResolveDialogOpen(false); + }; + + const handleAfterResolve = async () => { + if (!incidentId) return; + const updatedIncident = await fetchIncidentById(incidentId); + if (updatedIncident) { + setIncident(updatedIncident); + } + }; + + const renderContent = () => { + if (isLoading) { + return ( + + ); + } + + if (!incident) { + return ( + + ); + } + + return ( + + + + + + + + {isActive ? t("incidentsPage.active") : t("incidentsPage.resolved")} + + + + + + {t("incidentsPage.incidentItemMonitor")}: + + + {incident?.monitorId?.name || t("incidentsPage.unknownMonitor")} + + + + + {t("incidentsPage.URL")}: + + + {incident?.monitorId?.url || "-"} + + + + + + + + + + + + + + {t("incidentsPage.timeline")} + + + + + {!isActive && ( + + )} + + + + + + + + + + + {isActive + ? t("incidentsPage.errorDetails") + : t("incidentsPage.resolutionDetails")} + + + + + + + + {incident?.message || "-"} + + + {!isActive && ( + <> + + + + {incident?.resolutionType === "manual" && ( + <> + + + + + )} + + )} + + + + + + + + ); + }; + + return ( + + + {renderContent()} + + {isActive && ( + + )} + + + + + + + ); +}; + +IncidentDetailsModal.propTypes = { + open: PropTypes.bool.isRequired, + incidentId: PropTypes.string, + onClose: PropTypes.func.isRequired, + onResolved: PropTypes.func, +}; + +export default IncidentDetailsModal; diff --git a/client/src/Pages/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx b/client/src/Pages/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx index 59749fca7..2902da049 100644 --- a/client/src/Pages/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx +++ b/client/src/Pages/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx @@ -2,28 +2,25 @@ import IconButton from "@mui/material/IconButton"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; -import Settings from "../../../../../assets/icons/settings-bold.svg?react"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { Button, Stack } from "@mui/material"; +import Settings from "../../../../assets/icons/settings-bold.svg?react"; +import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; import useFetchIncidents from "../../hooks/useFetchIncidents"; // Utils import { useState } from "react"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; import { TypeToPathMap } from "@/Utils/monitorUtils.js"; +import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; -const IncidentActionsMenu = ({ incident, onResolve }) => { +const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => { const [anchorEl, setAnchorEl] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); - const [comment, setComment] = useState(""); const theme = useTheme(); const { t } = useTranslation(); - const navigate = useNavigate(); const { fetchIncidentById } = useFetchIncidents(); + const navigate = useNavigate(); const openMenu = (event) => { event.preventDefault(); event.stopPropagation(); @@ -39,7 +36,6 @@ const IncidentActionsMenu = ({ incident, onResolve }) => { const openResolveDialog = (e) => { closeMenu(e); - setComment(""); setIsDialogOpen(true); }; @@ -47,17 +43,6 @@ const IncidentActionsMenu = ({ incident, onResolve }) => { if (e) { e.stopPropagation(); } - setComment(""); - setIsDialogOpen(false); - }; - - const handleConfirmResolve = (e) => { - e.stopPropagation(); - if (onResolve) { - const options = comment.trim() ? { comment: comment.trim() } : {}; - onResolve(incident._id, options); - } - setComment(""); setIsDialogOpen(false); }; @@ -65,7 +50,9 @@ const IncidentActionsMenu = ({ incident, onResolve }) => { const handleGoToDetails = (e) => { e.stopPropagation(); closeMenu(e); - navigate(`/incidents/details/${incident._id}`); + if (onOpenDetails) { + onOpenDetails(incident._id); + } }; const handleGoToMonitor = async (e) => { @@ -128,6 +115,12 @@ const IncidentActionsMenu = ({ incident, onResolve }) => { }, }} > + + {t("incidentsPage.incidentsTableActionDetails", "Details")} + + + {t("incidentsPage.incidentsTableActionGoToMonitor", "Go to monitor")} + {isActive && ( { @@ -138,57 +131,13 @@ const IncidentActionsMenu = ({ incident, onResolve }) => { {t("incidentsPage.incidentsTableActionResolveManually")} )} - - {t("incidentsPage.incidentsTableActionDetails", "Details")} - - - {t("incidentsPage.incidentsTableActionGoToMonitor", "Go to monitor")} - - - Add comment - - {/* Future options will be added here */} - - - setComment(e.target.value)} - maxWidth="100%" - /> - - - - - - + onResolve={onResolve} + /> ); }; @@ -199,6 +148,7 @@ IncidentActionsMenu.propTypes = { status: PropTypes.bool.isRequired, }).isRequired, onResolve: PropTypes.func.isRequired, + onOpenDetails: PropTypes.func, }; export default IncidentActionsMenu; diff --git a/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx index 05d73f755..157cf5103 100644 --- a/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx @@ -26,6 +26,7 @@ const IncidentTable = ({ handleChangeRowsPerPage, resolveIncident, handleUpdateTrigger, + onOpenDetails, }) => { const uiTimezone = useSelector((state) => state.ui.timezone); @@ -125,6 +126,7 @@ const IncidentTable = ({ ); }, @@ -180,6 +182,7 @@ IncidentTable.propTypes = { handleChangeRowsPerPage: PropTypes.func.isRequired, // Handler for rows per page change resolveIncident: PropTypes.func.isRequired, handleUpdateTrigger: PropTypes.func.isRequired, + onOpenDetails: PropTypes.func, }; export default IncidentTable; diff --git a/client/src/Pages/Incidents2/Components/ResolveIncidentDialog/index.jsx b/client/src/Pages/Incidents2/Components/ResolveIncidentDialog/index.jsx new file mode 100644 index 000000000..e2be5fed2 --- /dev/null +++ b/client/src/Pages/Incidents2/Components/ResolveIncidentDialog/index.jsx @@ -0,0 +1,105 @@ +import { useEffect, useState } from "react"; +import PropTypes from "prop-types"; +import { Button, Stack } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@emotion/react"; +import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; + +const ResolveIncidentDialog = ({ + open, + incidentId, + onClose, + onResolve, + onAfterResolve, + onResolved, + isLoading, +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const [comment, setComment] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (open) { + setComment(""); + } + }, [open]); + + const handleConfirm = async () => { + if (!incidentId) return; + const trimmed = comment.trim(); + try { + setIsSubmitting(true); + await onResolve(incidentId, trimmed ? { comment: trimmed } : {}); + if (onAfterResolve) { + await onAfterResolve(); + } + onResolved?.(); + onClose(); + } catch (error) { + console.error("Error resolving incident:", error); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + setComment(e.target.value)} + maxWidth="100%" + /> + + + + + + + ); +}; + +ResolveIncidentDialog.propTypes = { + open: PropTypes.bool.isRequired, + incidentId: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onResolve: PropTypes.func.isRequired, + onAfterResolve: PropTypes.func, + isLoading: PropTypes.bool, + onResolved: PropTypes.func, +}; + +ResolveIncidentDialog.defaultProps = { + isLoading: false, +}; + +export default ResolveIncidentDialog; diff --git a/client/src/Pages/Incidents2/index.jsx b/client/src/Pages/Incidents2/index.jsx index 966388b51..e3bd53ab4 100644 --- a/client/src/Pages/Incidents2/index.jsx +++ b/client/src/Pages/Incidents2/index.jsx @@ -5,6 +5,7 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import IncidentTable from "./Components/IncidentTable/index.jsx"; import OptionsHeader from "./Components/OptionsHeader/index.jsx"; import IncidentsSummaryPanel from "./Components/IncidentsSummaryPanel/index.jsx"; +import IncidentDetailsModal from "./Components/IncidentDetailsModal/index.jsx"; //Utils import { useTheme } from "@emotion/react"; @@ -30,6 +31,8 @@ const Incidents2 = () => { const [rowsPerPage, setRowsPerPage] = useState(10); const [monitorLookup, setMonitorLookup] = useState(undefined); const [updateTrigger, setUpdateTrigger] = useState(false); + const [isDetailsOpen, setIsDetailsOpen] = useState(false); + const [selectedIncidentId, setSelectedIncidentId] = useState(null); const handleUpdateTrigger = () => { setUpdateTrigger((prev) => !prev); }; @@ -114,6 +117,17 @@ const Incidents2 = () => { setPage(0); }; + const handleOpenDetails = (id) => { + if (!id) return; + setSelectedIncidentId(id); + setIsDetailsOpen(true); + }; + + const handleCloseDetails = () => { + setIsDetailsOpen(false); + setSelectedIncidentId(null); + }; + return ( @@ -142,6 +156,13 @@ const Incidents2 = () => { handleChangeRowsPerPage={handleChangeRowsPerPage} resolveIncident={resolveIncident} handleUpdateTrigger={handleUpdateTrigger} + onOpenDetails={handleOpenDetails} + /> + ); diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 38c29fa4a..ad74b692e 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -29,10 +29,8 @@ import InfrastructureDetails from "../Pages/Infrastructure/Details/index.jsx"; import ServerUnreachable from "../Pages/ServerUnreachable.jsx"; // Incidents -import Checks from "../Pages/v1/Incidents"; +import Checks from "../Pages/Incidents/index.jsx"; import Incidents2 from "../Pages/Incidents2/index.jsx"; -import IncidentDetails from "../Pages/v1/Incidents2/Details"; - // Status pages import CreateStatus from "../Pages/StatusPage/Create/index.jsx"; @@ -136,10 +134,6 @@ const Routes = () => { path="checks/:monitorId?" element={} /> - } - /> } diff --git a/client/src/locales/en.json b/client/src/locales/en.json index ad68322ff..c543eb7e5 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -247,6 +247,7 @@ "addMonitors": "Add monitors", "window": "window", "cancel": "Cancel", + "close": "Close", "message": "Message", "low": "low", "high": "high", @@ -1203,6 +1204,23 @@ "hours": "hours", "loading": "Loading incident details...", "incidentNotFound": "Incident not found", - "incidentDetails": "Incident Details" + "incidentDetails": "INCIDENT DETAILS", + "activeIncident": "ACTIVE INCIDENT", + "resolvedIncident": "RESOLVED INCIDENT", + "overview": "OVERVIEW", + "URL": "URL", + "incidentAnalysis": "INCIDENT ANALYSIS", + "timeline": "Timeline", + "startedAt": "Started at", + "endedAt": "Ended at", + "downtime": "Downtime", + "errorDetails": "Error details", + "resolutionDetails": "Resolution details", + "statusCode": "Status code", + "resolutionMethod": "Resolution method", + "resolvedBy": "Resolved by", + "comment": "Comment", + "noCommentProvided": "No comment provided", + "loadingIncidentDetails": "Loading incident details..." } }