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 }) => {
},
}}
>
+
+
{isActive && (
)}
-
-
-
- {/* 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..."
}
}