From 1c01dcaf31e0fcdc7cfeba5a3f11ff52e3be7b21 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Wed, 14 Jan 2026 22:12:26 -0500 Subject: [PATCH] create incidents action menu and detail page --- .../IncidentTable/IncidentActionsMenu.jsx | 204 ++++++++++++++++++ .../Components/IncidentTable/index.jsx | 39 +--- .../LatestIncidentsPanel/IncidentItem.jsx | 28 +-- .../src/Pages/v1/Incidents2/Details/index.jsx | 171 +++++++++++++++ .../hooks/useGetIncidentsDuration.js | 25 +++ client/src/Routes/index.jsx | 5 + client/src/locales/en.json | 12 +- 7 files changed, 428 insertions(+), 56 deletions(-) create mode 100644 client/src/Pages/v1/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx create mode 100644 client/src/Pages/v1/Incidents2/Details/index.jsx create mode 100644 client/src/Pages/v1/Incidents2/hooks/useGetIncidentsDuration.js 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, + }, + }} + > + + + + + {isActive && ( + { + e.stopPropagation(); + openResolveDialog(e); + }} + > + {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%" + /> + + + + + + + + ); +}; + +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" } }