diff --git a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx
new file mode 100644
index 000000000..e4dad7c3e
--- /dev/null
+++ b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx
@@ -0,0 +1,404 @@
+// 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 "../SummaryCard/index.jsx";
+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 "../../hooks/useFetchIncidents.js";
+import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration.js";
+
+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/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx b/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx
new file mode 100644
index 000000000..28b3a4f52
--- /dev/null
+++ b/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx
@@ -0,0 +1,156 @@
+// Components
+import IconButton from "@mui/material/IconButton";
+import Menu from "@mui/material/Menu";
+import MenuItem from "@mui/material/MenuItem";
+import Icon from "@/Components/v1/Icon/index.jsx";
+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 { TypeToPathMap } from "@/Utils/monitorUtils.js";
+import { useNavigate } from "react-router-dom";
+import PropTypes from "prop-types";
+
+const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const { fetchIncidentById } = useFetchIncidents();
+ const navigate = useNavigate();
+ const openMenu = (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ setAnchorEl(event.currentTarget);
+ };
+
+ const closeMenu = (e) => {
+ if (e) {
+ e.stopPropagation();
+ }
+ setAnchorEl(null);
+ };
+
+ const openResolveDialog = (e) => {
+ closeMenu(e);
+ setIsDialogOpen(true);
+ };
+
+ const closeDialog = (e) => {
+ if (e) {
+ e.stopPropagation();
+ }
+ setIsDialogOpen(false);
+ };
+
+ const isActive = incident.status === true;
+ const handleGoToDetails = (e) => {
+ e.stopPropagation();
+ closeMenu(e);
+ if (onOpenDetails) {
+ onOpenDetails(incident.id);
+ }
+ };
+
+ const handleGoToMonitor = async (e) => {
+ e.stopPropagation();
+ closeMenu(e);
+ try {
+ const incidentData = await fetchIncidentById(incident.id);
+ if (incidentData) {
+ const { type, _id: monitorId } = incidentData.monitorId;
+ const path = TypeToPathMap[type];
+ if (path && monitorId) {
+ navigate(`/${path}/${monitorId}`);
+ } else {
+ console.error(`Monitor type not found`);
+ }
+ } else {
+ console.error("Monitor information not found in incident data");
+ }
+ } catch (error) {
+ console.error("Error fetching incident details:", error);
+ }
+ };
+
+ return (
+ <>
+ {
+ event.stopPropagation();
+ openMenu(event);
+ }}
+ sx={{
+ "&:focus": {
+ outline: "none",
+ },
+ "& svg path": {
+ stroke: theme.palette.primary.contrastTextTertiary,
+ },
+ }}
+ >
+
+
+
+
+
+ >
+ );
+};
+
+IncidentActionsMenu.propTypes = {
+ incident: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ status: PropTypes.bool.isRequired,
+ }).isRequired,
+ onResolve: PropTypes.func.isRequired,
+ onOpenDetails: PropTypes.func,
+};
+
+export default IncidentActionsMenu;
diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
index 670da4a81..7d23d8ebd 100644
--- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
+++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
@@ -6,13 +6,14 @@ import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx";
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
+import IncidentActionsMenu from "./IncidentActionsMenu.jsx";
//Utils
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
-import { Button, Typography, useTheme } from "@mui/material";
+import { Typography, useTheme } from "@mui/material";
const IncidentTable = ({
monitors = [],
@@ -26,15 +27,16 @@ const IncidentTable = ({
handleChangeRowsPerPage,
resolveIncident,
handleUpdateTrigger,
+ onOpenDetails,
}) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const { t } = useTranslation();
const theme = useTheme();
- const handleResolveIncident = async (incidentId) => {
+ const handleResolveIncident = async (incidentId, options = {}) => {
try {
- await resolveIncident(incidentId);
+ await resolveIncident(incidentId, options);
handleUpdateTrigger();
} catch (error) {
console.error(t("incidentsPage.errorResolvingIncident"), error);
@@ -46,7 +48,6 @@ const IncidentTable = ({
id: "monitorName",
content: t("incidentsTableMonitorName"),
render: (row) => {
- console.log(monitors, row);
const monitor = monitors.find((monitor) => monitor.id === row.monitorId);
return monitor ? monitor.name : "N/A";
},
@@ -125,32 +126,13 @@ const IncidentTable = ({
id: "action",
content: t("actions"),
render: (row) => {
- if (row.status === true) {
- return (
-
- );
- } else {
- return (
-
- {t("incidentsPage.incidentsTableResolved")}
-
- );
- }
+ return (
+
+ );
},
},
];
@@ -204,6 +186,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/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx
index 0ecb34791..7c6fb611a 100644
--- a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx
+++ b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx
@@ -5,36 +5,13 @@ import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { getHumanReadableDuration } from "@/Utils/timeUtils.js";
import Icon from "@/Components/v1/Icon";
import { useTranslation } from "react-i18next";
+import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration";
const IncidentItem = ({ incident }) => {
const { t } = useTranslation();
const theme = useTheme();
const isActive = incident.status === true;
- const calculateDuration = () => {
- if (!incident.startTime) {
- return "-";
- }
- const startTime = new Date(incident.startTime);
- const endTime = isActive
- ? new Date()
- : incident.endTime
- ? new Date(incident.endTime)
- : null;
-
- if (!endTime) {
- return "-";
- }
-
- const durationMs = endTime.getTime() - startTime.getTime();
-
- if (durationMs < 0) {
- return "-";
- }
-
- return getHumanReadableDuration(durationMs);
- };
-
- const duration = calculateDuration();
+ const duration = useGetIncidentsDuration(incident, isActive);
const iconWrapperStyle = {
display: "flex",
justifyContent: "center",
diff --git a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx
index d24bcd0ef..ff1ef9417 100644
--- a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx
+++ b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx
@@ -84,7 +84,7 @@ const LatestIncidentsPanel = ({ incidents = [], isLoading = false, error = null
) : (
{incidents.map((incident, index) => (
-
+
{index < incidents.length - 1 && }
diff --git a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx
index 3a0ca1985..8d7439576 100644
--- a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx
+++ b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx
@@ -26,6 +26,7 @@ const OptionsHeader = ({
{ _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") },
{ _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") },
{ _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") },
+ { _id: "automatic", name: t("incidentsPage.incidentsOptionsHeaderFilterAutomatic") },
];
const stackStyles = {
diff --git a/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx b/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx
new file mode 100644
index 000000000..3e868b2dc
--- /dev/null
+++ b/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx
@@ -0,0 +1,101 @@
+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 = false,
+}) => {
+ 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,
+ onClose: PropTypes.func.isRequired,
+ onResolve: PropTypes.func.isRequired,
+ onAfterResolve: PropTypes.func,
+ isLoading: PropTypes.bool,
+ onResolved: PropTypes.func,
+};
+
+export default ResolveIncidentDialog;
diff --git a/client/src/Pages/Incidents/hooks/useFetchIncidents.js b/client/src/Pages/Incidents/hooks/useFetchIncidents.js
index 54874bae7..e308c9319 100644
--- a/client/src/Pages/Incidents/hooks/useFetchIncidents.js
+++ b/client/src/Pages/Incidents/hooks/useFetchIncidents.js
@@ -72,6 +72,16 @@ const useFetchIncidents = () => {
[fetchIncidentsByTeam]
);
+ const fetchIncidentsByResolutionType = useCallback(
+ async (resolutionType, config = {}) => {
+ await fetchIncidentsByTeam({
+ resolutionType,
+ sortOrder: "desc",
+ ...config,
+ });
+ },
+ [fetchIncidentsByTeam]
+ );
/**
* Fetch resolved incidents
*
@@ -198,6 +208,7 @@ const useFetchIncidents = () => {
fetchIncidentById,
resolveIncident,
fetchIncidentSummary,
+ fetchIncidentsByResolutionType,
};
};
diff --git a/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js b/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js
new file mode 100644
index 000000000..e9b1da6d1
--- /dev/null
+++ b/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js
@@ -0,0 +1,25 @@
+import { getHumanReadableDuration } from "@/Utils/timeUtils.js";
+const useGetIncidentsDuration = (incident, isActive) => {
+ if (!incident?.startTime) {
+ return "-";
+ }
+ const startTime = new Date(incident?.startTime);
+ const endTime = isActive
+ ? new Date()
+ : incident?.endTime
+ ? new Date(incident.endTime)
+ : null;
+
+ if (!endTime) {
+ return "-";
+ }
+
+ const durationMs = endTime.getTime() - startTime.getTime();
+
+ if (durationMs < 0) {
+ return "-";
+ }
+
+ return getHumanReadableDuration(durationMs);
+};
+export default useGetIncidentsDuration;
diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx
index 4e68279c5..3937512b6 100644
--- a/client/src/Pages/Incidents/index.jsx
+++ b/client/src/Pages/Incidents/index.jsx
@@ -5,7 +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";
import { useState, useEffect } from "react";
@@ -30,6 +30,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);
};
@@ -47,6 +49,7 @@ const Incidents2 = () => {
fetchActiveIncidents,
fetchResolvedIncidents,
resolveIncident,
+ fetchIncidentsByResolutionType,
} = useFetchIncidents();
const networkError = monitorsNetworkError || incidentsNetworkError;
@@ -70,6 +73,10 @@ const Incidents2 = () => {
fetchActiveIncidents(config);
} else if (filter === "resolved") {
fetchResolvedIncidents(config);
+ } else if (filter === "manual") {
+ fetchIncidentsByResolutionType("manual", config);
+ } else if (filter === "automatic") {
+ fetchIncidentsByResolutionType("automatic", config);
} else {
fetchIncidents(config);
}
@@ -83,6 +90,7 @@ const Incidents2 = () => {
fetchActiveIncidents,
fetchResolvedIncidents,
fetchIncidents,
+ fetchIncidentsByResolutionType,
]);
useEffect(() => {
@@ -113,6 +121,16 @@ const Incidents2 = () => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
+ const handleOpenDetails = (id) => {
+ if (!id) return;
+ setSelectedIncidentId(id);
+ setIsDetailsOpen(true);
+ };
+
+ const handleCloseDetails = () => {
+ setIsDetailsOpen(false);
+ setSelectedIncidentId(null);
+ };
return (
@@ -143,6 +161,13 @@ const Incidents2 = () => {
handleChangeRowsPerPage={handleChangeRowsPerPage}
resolveIncident={resolveIncident}
handleUpdateTrigger={handleUpdateTrigger}
+ onOpenDetails={handleOpenDetails}
+ />
+
);
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 63c4056a6..4f4447b86 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -249,6 +249,7 @@
"addMonitors": "Add monitors",
"window": "window",
"cancel": "Cancel",
+ "close": "Close",
"message": "Message",
"low": "low",
"high": "high",
@@ -1201,12 +1202,40 @@
"noIncidentIdProvided": "No incident ID provided",
"errorFetchingIncidentSummary": "Error fetching incident summary",
"incidentsOptionsHeaderFilterManual": "Manual",
+ "incidentsOptionsHeaderFilterAutomatic": "Automatic",
"incidentsOptionsHeaderFilterAll": "All",
"incidentsOptionsHeaderFilterActive": "Active",
"incidentsOptionsHeaderFilterResolved": "Resolved",
"incidentsTableResolved": "Closed",
"incidentsTableActionResolveManually": "Resolve Manually",
+ "incidentsTableActionDetails": "Details",
+ "incidentsTableActionGoToMonitor": "Go to monitor",
+ "resolveIncidentDialogTitle": "Resolve Incident",
+ "resolveIncidentDialogDescription": "Are you sure you want to resolve this incident manually?",
+ "resolveIncidentDialogCommentLabel": "Comment (optional)",
+ "resolveIncidentDialogCommentPlaceholder": "Add a comment about the resolution...",
+ "resolveIncidentDialogConfirm": "Resolve",
"hours": "hours",
+ "loading": "Loading incident details...",
+ "incidentNotFound": "Incident not found",
+ "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...",
"none": "None"
}
}