diff --git a/client/src/Pages/v1/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx b/client/src/Pages/v1/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx
new file mode 100644
index 000000000..59749fca7
--- /dev/null
+++ b/client/src/Pages/v1/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx
@@ -0,0 +1,204 @@
+// Components
+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 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 PropTypes from "prop-types";
+
+const IncidentActionsMenu = ({ incident, onResolve }) => {
+ 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 openMenu = (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ setAnchorEl(event.currentTarget);
+ };
+
+ const closeMenu = (e) => {
+ if (e) {
+ e.stopPropagation();
+ }
+ setAnchorEl(null);
+ };
+
+ const openResolveDialog = (e) => {
+ closeMenu(e);
+ setComment("");
+ setIsDialogOpen(true);
+ };
+
+ const closeDialog = (e) => {
+ 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);
+ };
+
+ const isActive = incident.status === true;
+ const handleGoToDetails = (e) => {
+ e.stopPropagation();
+ closeMenu(e);
+ navigate(`/incidents/details/${incident._id}`);
+ };
+
+ const handleGoToMonitor = async (e) => {
+ e.stopPropagation();
+ closeMenu(e);
+ try {
+ const incidentData = await fetchIncidentById(incident._id);
+ console.log(incidentData);
+ 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,
+ },
+ }}
+ >
+
+
+
+
+
+
+ setComment(e.target.value)}
+ maxWidth="100%"
+ />
+
+
+
+
+
+
+ >
+ );
+};
+
+IncidentActionsMenu.propTypes = {
+ incident: PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ status: PropTypes.bool.isRequired,
+ }).isRequired,
+ onResolve: PropTypes.func.isRequired,
+};
+
+export default IncidentActionsMenu;
diff --git a/client/src/Pages/v1/Incidents2/Components/IncidentTable/index.jsx b/client/src/Pages/v1/Incidents2/Components/IncidentTable/index.jsx
index 8cac4e07b..5456d55f2 100644
--- a/client/src/Pages/v1/Incidents2/Components/IncidentTable/index.jsx
+++ b/client/src/Pages/v1/Incidents2/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 = ({
incidents = [],
@@ -31,9 +32,9 @@ const IncidentTable = ({
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);
@@ -120,32 +121,12 @@ const IncidentTable = ({
id: "action",
content: t("actions"),
render: (row) => {
- if (row.status === true) {
- return (
-
- );
- } else {
- return (
-
- {t("incidentsPage.incidentsTableResolved")}
-
- );
- }
+ return (
+
+ );
},
},
];
diff --git a/client/src/Pages/v1/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx b/client/src/Pages/v1/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx
index c212c8e52..dff710597 100644
--- a/client/src/Pages/v1/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx
+++ b/client/src/Pages/v1/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx
@@ -2,40 +2,16 @@ import PropTypes from "prop-types";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
-import { getHumanReadableDuration } from "@/Utils/timeUtils.js";
import Monitors from "@/assets/icons/monitors.svg?react";
import AverageResponseIcon from "@/assets/icons/status-pages.svg?react";
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 = {
px: theme.spacing(2),
display: "flex",
diff --git a/client/src/Pages/v1/Incidents2/Details/index.jsx b/client/src/Pages/v1/Incidents2/Details/index.jsx
new file mode 100644
index 000000000..67388899d
--- /dev/null
+++ b/client/src/Pages/v1/Incidents2/Details/index.jsx
@@ -0,0 +1,171 @@
+// Components
+import { Stack, Typography, Grid, Box } from "@mui/material";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
+import SummaryCard from "../Components/SummaryCard";
+import useGetIncidentsDuration from "../hooks/useGetIncidentsDuration";
+
+// Utils
+import { useParams } from "react-router-dom";
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import useFetchIncidents from "../hooks/useFetchIncidents";
+import { useEffect, useState } from "react";
+
+// Constants
+const BREADCRUMBS = [
+ { name: "incidents", path: "/incidents" },
+ { name: "details", path: "" },
+];
+
+const IncidentDetails = () => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const { incidentId } = useParams();
+ const { fetchIncidentById } = useFetchIncidents();
+ const [incident, setIncident] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const isActive = incident?.status === true;
+ const duration = useGetIncidentsDuration(incident || null, isActive);
+ useEffect(() => {
+ const loadIncident = async () => {
+ if (incidentId) {
+ setIsLoading(true);
+ try {
+ const incidentData = await fetchIncidentById(incidentId);
+ console.log(incidentData);
+ setIncident(incidentData);
+ } catch (error) {
+ console.error("Error fetching incident:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ loadIncident();
+ }, [incidentId, fetchIncidentById]);
+
+ if (isLoading) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (!incident) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {t("incidentsPage.incidentDetails", "Incident Details")}
+
+
+
+
+
+ {duration}
+
+
+
+
+ {incident?.startTime}
+
+
+ {!isActive && (
+
+
+ {incident?.endTime}
+
+
+ )}
+
+
+
+
+ {incident?.statusCode}
+ {incident?.message}
+
+
+
+
+ {incident?.checks.length}
+
+
+ {!isActive && (
+
+
+ {incident?.resolutionType}
+ {incident?.resolutionType === "manual" && (
+
+
+ Resolved by {incident?.resolvedBy?.email}
+
+ {incident?.comment && (
+ Comment: {incident?.comment}
+ )}
+
+ )}
+
+
+ )}
+
+
+
+ );
+};
+
+export default IncidentDetails;
diff --git a/client/src/Pages/v1/Incidents2/hooks/useGetIncidentsDuration.js b/client/src/Pages/v1/Incidents2/hooks/useGetIncidentsDuration.js
new file mode 100644
index 000000000..e9b1da6d1
--- /dev/null
+++ b/client/src/Pages/v1/Incidents2/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/Routes/index.jsx b/client/src/Routes/index.jsx
index 255d913c1..1af600e09 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -31,6 +31,7 @@ import ServerUnreachable from "../Pages/v1/ServerUnreachable.jsx";
// Incidents
import Checks from "../Pages/v1/Incidents";
import Incidents2 from "../Pages/v1/Incidents2";
+import IncidentDetails from "../Pages/v1/Incidents2/Details";
// Status pages
import CreateStatus from "../Pages/v1/StatusPage/Create";
@@ -134,6 +135,10 @@ const Routes = () => {
path="checks/:monitorId?"
element={}
/>
+ }
+ />
}
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index ed07fd8d2..ad68322ff 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -1193,6 +1193,16 @@
"incidentsOptionsHeaderFilterResolved": "Resolved",
"incidentsTableResolved": "Closed",
"incidentsTableActionResolveManually": "Resolve Manually",
- "hours": "hours"
+ "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"
}
}