create incidents action menu and detail page

This commit is contained in:
karenvicent committed 2026-01-14 22:12:26 -05:00
1 parent 668ea7418d
commit 1c01dcaf31
7 files changed
+428 -56

No files matched your search

@@ -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 (
<>
<IconButton
aria-label="incident actions"
onClick={(event) => {
event.stopPropagation();
openMenu(event);
}}
sx={{
"&:focus": {
outline: "none",
},
"& svg path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Settings />
</IconButton>
<Menu
className="incident-actions-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={closeMenu}
disableScrollLock
slotProps={{
paper: {
sx: {
"& ul": {
p: theme.spacing(2.5),
backgroundColor: theme.palette.primary.main,
},
"& li": { m: 0, color: theme.palette.primary.contrastTextSecondary },
},
},
}}
>
{isActive && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
openResolveDialog(e);
}}
>
{t("incidentsPage.incidentsTableActionResolveManually")}
</MenuItem>
)}
<MenuItem onClick={handleGoToDetails}>
{t("incidentsPage.incidentsTableActionDetails", "Details")}
</MenuItem>
<MenuItem onClick={handleGoToMonitor}>
{t("incidentsPage.incidentsTableActionGoToMonitor", "Go to monitor")}
</MenuItem>
<MenuItem
//onClick={handleResolve}
>
Add comment
</MenuItem>
{/* Future options will be added here */}
</Menu>
<GenericDialog
open={isDialogOpen}
theme={theme}
title={t("incidentsPage.resolveIncidentDialogTitle")}
description={t("incidentsPage.resolveIncidentDialogDescription")}
onClose={closeDialog}
>
<Stack gap={theme.spacing(4)}>
<TextInput
label={t("incidentsPage.resolveIncidentDialogCommentLabel")}
placeholder={t("incidentsPage.resolveIncidentDialogCommentPlaceholder")}
value={comment}
onChange={(e) => setComment(e.target.value)}
maxWidth="100%"
/>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(4)}
justifyContent="flex-end"
>
<Button
variant="contained"
color="secondary"
onClick={closeDialog}
>
{t("cancel", "Cancel")}
</Button>
<Button
variant="contained"
color="error"
onClick={handleConfirmResolve}
>
{t("incidentsPage.resolveIncidentDialogConfirm")}
</Button>
</Stack>
</Stack>
</GenericDialog>
</>
);
};
IncidentActionsMenu.propTypes = {
incident: PropTypes.shape({
_id: PropTypes.string.isRequired,
status: PropTypes.bool.isRequired,
}).isRequired,
onResolve: PropTypes.func.isRequired,
};
export default IncidentActionsMenu;
@@ -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 (
<Button
variant="contained"
color="accent"
sx={{
minHeight: "max-content",
lineHeight: 1.2,
}}
onClick={() => {
handleResolveIncident(row._id);
}}
>
{t("incidentsPage.incidentsTableActionResolveManually")}
</Button>
);
} else {
return (
<Typography
variant="body2"
color={theme.palette.primary.contrastTextSecondary}
>
{t("incidentsPage.incidentsTableResolved")}
</Typography>
);
}
return (
<IncidentActionsMenu
incident={row}
onResolve={handleResolveIncident}
/>
);
},
},
];
@@ -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",
@@ -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 (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<GenericFallback
isLoading={true}
message={t("incidentsPage.loading", "Loading incident details...")}
/>
</Stack>
);
}
if (!incident) {
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<GenericFallback
isLoading={false}
message={t("incidentsPage.incidentNotFound", "Incident not found")}
/>
</Stack>
);
}
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<Typography variant="h2">
{t("incidentsPage.incidentDetails", "Incident Details")}
</Typography>
<Stack gap={theme.spacing(10)}>
<Grid
container
spacing={3}
>
<Grid
item
xs={12}
sm={4}
>
<SummaryCard
title="Downtime duration"
isHighPriority={isActive}
sx={{
backgroundColor: theme.palette.primary.lowContrast,
color: theme.palette.primary.contrastTextSecondary,
}}
>
<Typography variant="h6">{duration}</Typography>
</SummaryCard>
</Grid>
<Grid
item
xs={12}
sm={4}
>
<SummaryCard title="Started at">
<Typography variant="h6">{incident?.startTime}</Typography>
</SummaryCard>
</Grid>
{!isActive && (
<Grid
item
xs={12}
sm={4}
>
<SummaryCard title="Ended at">
<Typography variant="h6">{incident?.endTime}</Typography>
</SummaryCard>
</Grid>
)}
</Grid>
<Grid
container
spacing={3}
>
<Grid
item
xs={12}
sm={4}
>
<SummaryCard title="Error details">
<Typography variant="h6">{incident?.statusCode}</Typography>
<Typography variant="h6">{incident?.message}</Typography>
</SummaryCard>
</Grid>
<Grid
item
xs={12}
sm={4}
>
<SummaryCard title="Total Checks Failed">
<Typography variant="h6">{incident?.checks.length}</Typography>
</SummaryCard>
</Grid>
{!isActive && (
<Grid
item
xs={12}
sm={4}
>
<SummaryCard title="Resolution Method">
<Typography variant="h6">{incident?.resolutionType}</Typography>
{incident?.resolutionType === "manual" && (
<Stack>
<Typography variant="h6">
Resolved by {incident?.resolvedBy?.email}
</Typography>
{incident?.comment && (
<Typography variant="h6">Comment: {incident?.comment}</Typography>
)}
</Stack>
)}
</SummaryCard>
</Grid>
)}
</Grid>
</Stack>
</Stack>
);
};
export default IncidentDetails;
@@ -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;
+5
View File
@@ -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={<Checks />}
/>
<Route
path="incidents/details/:incidentId"
element={<IncidentDetails />}
/>
<Route
path="incidents/:monitorId?"
element={<Incidents2 />}
+11 -1
View File
@@ -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"
}
}