incidents feature fron-end
This commit is contained in:
1 parent
fec32380ae
commit
fd12ca6e21
19 files changed
+1774
-42
No files matched your search
@@ -48,6 +48,7 @@ const getMenu = (t) => [
|
||||
path: "notifications",
|
||||
icon: <Notifications />,
|
||||
},
|
||||
{ name: t("menu.checks"), path: "checks", icon: <Docs /> },
|
||||
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
|
||||
|
||||
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
|
||||
|
||||
@@ -18,13 +18,11 @@ import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
//Constants
|
||||
const Incidents = () => {
|
||||
const Checks = () => {
|
||||
// Redux state
|
||||
const { t } = useTranslation();
|
||||
|
||||
const BREADCRUMBS = [
|
||||
{ name: t("incidentsPageTitle", "Incidents"), path: "/incidents" },
|
||||
];
|
||||
const BREADCRUMBS = [{ name: t("checksPageTitle"), path: "/checks" }];
|
||||
|
||||
// Local state
|
||||
const [selectedMonitor, setSelectedMonitor] = useState("0");
|
||||
@@ -86,8 +84,8 @@ const Incidents = () => {
|
||||
disabled={isLoadingAcknowledge}
|
||||
>
|
||||
{selectedMonitor === "0"
|
||||
? t("incidentsPageActionResolveAll")
|
||||
: t("incidentsPageActionResolveMonitor")}
|
||||
? t("checksPageActionResolveAllMonitor")
|
||||
: t("checksPageActionResolveMonitor")}
|
||||
</Button>
|
||||
</Box>
|
||||
<StatusBoxes
|
||||
@@ -117,4 +115,4 @@ const Incidents = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Incidents;
|
||||
export default Checks;
|
||||
@@ -0,0 +1,145 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
|
||||
import Incidents from "@/assets/icons/incidents.svg?react";
|
||||
import CheckIcon from "@/assets/icons/check-icon.svg?react";
|
||||
import SummaryCard from "../SummaryCard/index.jsx";
|
||||
|
||||
/**
|
||||
* ActiveIncidentsPanel Component
|
||||
*
|
||||
* Displays a quick overview of currently active incidents.
|
||||
* Shows only essential information: monitor name, status badge, and duration.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Array} props.incidents - Array of active incidents
|
||||
* @param {number} props.totalCount - Total count of active incidents (for badge)
|
||||
* @param {boolean} props.isLoading - Loading state
|
||||
* @param {Object} props.error - Error object if any
|
||||
* @param {Function} props.onIncidentClick - Callback when incident is clicked (optional)
|
||||
*/
|
||||
const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (isLoading) {
|
||||
return <PanelSkeleton />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<SummaryCard>
|
||||
<Typography
|
||||
color="error"
|
||||
align="center"
|
||||
>
|
||||
{t("incidentsPage.incidentsActivePanelError")}
|
||||
</Typography>
|
||||
</SummaryCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (!totalCount || totalCount === 0) {
|
||||
return (
|
||||
<SummaryCard>
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
padding={theme.spacing(10)}
|
||||
gap={theme.spacing(4)}
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
color: theme.palette.success.main,
|
||||
"& svg": {
|
||||
width: 60,
|
||||
height: 60,
|
||||
|
||||
"& path": { stroke: "currentColor", strokeWidth: 2 },
|
||||
},
|
||||
mb: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<CheckIcon />
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="h1"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
sx={{
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 600,
|
||||
textAlign: "center",
|
||||
color: theme.palette.success.lowContrast,
|
||||
letterSpacing: theme.spacing(0.4),
|
||||
}}
|
||||
>
|
||||
{t("incidentsPage.allSystemsAreOperational")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</SummaryCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SummaryCard isHighPriority={true}>
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
spacing={theme.spacing(4)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
color: theme.palette.error.lowContrast,
|
||||
padding: theme.spacing(2),
|
||||
"& svg": {
|
||||
width: 60,
|
||||
height: 60,
|
||||
|
||||
"& path": { stroke: "currentColor", strokeWidth: 2 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Incidents />
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="h1"
|
||||
sx={{
|
||||
fontSize: `calc(${theme.typography.h1.fontSize} * 2.5)`,
|
||||
fontWeight: 700,
|
||||
color: theme.palette.error.lowContrast,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{totalCount}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 700,
|
||||
letterSpacing: theme.spacing(0.4),
|
||||
paddingTop: theme.spacing(3),
|
||||
}}
|
||||
>
|
||||
{t("incidentsPage.incidentsActivePanelTitle")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</SummaryCard>
|
||||
);
|
||||
};
|
||||
|
||||
ActiveIncidentsPanel.propTypes = {
|
||||
totalCount: PropTypes.number,
|
||||
isLoading: PropTypes.bool,
|
||||
error: PropTypes.object,
|
||||
onIncidentClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActiveIncidentsPanel;
|
||||
@@ -0,0 +1,200 @@
|
||||
//Components
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import TableSkeleton from "@/Components/v1/Table/skeleton.jsx";
|
||||
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
|
||||
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";
|
||||
|
||||
//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";
|
||||
|
||||
const IncidentTable = ({
|
||||
incidents = [],
|
||||
incidentsCount = 0,
|
||||
isLoading = false,
|
||||
networkError = false,
|
||||
page = 0,
|
||||
rowsPerPage = 10,
|
||||
handleChangePage,
|
||||
handleChangeRowsPerPage,
|
||||
resolveIncident,
|
||||
handleUpdateTrigger,
|
||||
}) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const handleResolveIncident = async (incidentId) => {
|
||||
await resolveIncident(incidentId);
|
||||
handleUpdateTrigger();
|
||||
};
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "monitorName",
|
||||
content: t("incidentsTableMonitorName"),
|
||||
render: (row) => row.monitorName ?? "N/A",
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("incidentsTableStatus"),
|
||||
render: (row) => {
|
||||
const status = row.status === true ? "down" : "up";
|
||||
const statusText =
|
||||
row.status === true ? t("incidentsPage.active") : t("incidentsPage.resolved");
|
||||
return (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={statusText}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "startTime",
|
||||
content: t("incidentsPage.startTime"),
|
||||
render: (row) => {
|
||||
const formattedDate = formatDateWithTz(
|
||||
row.startTime || row.createdAt,
|
||||
"YYYY-MM-DD HH:mm:ss A",
|
||||
uiTimezone
|
||||
);
|
||||
return formattedDate;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "endTime",
|
||||
content: t("incidentsPage.endTime"),
|
||||
render: (row) => {
|
||||
if (row.endTime) {
|
||||
return formatDateWithTz(row.endTime, "YYYY-MM-DD HH:mm:ss A", uiTimezone);
|
||||
}
|
||||
return "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "resolutionType",
|
||||
content: t("incidentsPage.resolutionType"),
|
||||
render: (row) => {
|
||||
if (row.resolutionType) {
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
color:
|
||||
row.resolutionType === "manual"
|
||||
? theme.palette.accent.main
|
||||
: theme.palette.success.main,
|
||||
}}
|
||||
>
|
||||
{row.resolutionType}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
return "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "statusCode",
|
||||
content: t("incidentsTableStatusCode"),
|
||||
render: (row) => <HttpStatusLabel status={row.statusCode} />,
|
||||
},
|
||||
{
|
||||
id: "message",
|
||||
content: t("incidentsTableMessage"),
|
||||
render: (row) => row.message || "-",
|
||||
},
|
||||
{
|
||||
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>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (isLoading) return <TableSkeleton />;
|
||||
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<NetworkError />
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && incidents == undefined) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
{t("incidentsTableNoIncidents", "No incidents found")}
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
const incidentsData = Array.isArray(incidents) ? incidents : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={incidentsData}
|
||||
/>
|
||||
<Pagination
|
||||
paginationLabel={t("incidentsTablePaginationLabel", "Incidents")}
|
||||
itemCount={incidentsCount || 0}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
IncidentTable.propTypes = {
|
||||
incidents: PropTypes.array.isRequired, // Array of incident objects
|
||||
incidentsCount: PropTypes.number.isRequired, // Total count for pagination
|
||||
isLoading: PropTypes.bool.isRequired, // Loading state
|
||||
networkError: PropTypes.bool, // Network error object
|
||||
page: PropTypes.number.isRequired, // Current page number
|
||||
rowsPerPage: PropTypes.number.isRequired, // Number of rows per page
|
||||
handleChangePage: PropTypes.func.isRequired, // Handler for page change
|
||||
handleChangeRowsPerPage: PropTypes.func.isRequired, // Handler for rows per page change
|
||||
resolveIncident: PropTypes.func.isRequired,
|
||||
handleUpdateTrigger: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default IncidentTable;
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useEffect, useState } from "react";
|
||||
// Hooks
|
||||
import useFetchIncidents from "../../hooks/useFetchIncidents.js";
|
||||
import PropTypes from "prop-types";
|
||||
// Components
|
||||
import { Grid } from "@mui/material";
|
||||
import ActiveIncidentsPanel from "../ActiveIncidentsPanel/index.jsx";
|
||||
import LatestIncidentsPanel from "../LatestIncidentsPanel/index.jsx";
|
||||
import StatisticsPanel from "../StatisticsPanel/index.jsx";
|
||||
|
||||
const IncidentsSummaryPanel = ({ updateTrigger }) => {
|
||||
const { fetchIncidentSummary } = useFetchIncidents();
|
||||
const [summary, setSummary] = useState({
|
||||
totalActive: 0,
|
||||
avgResolutionTimeHours: 0,
|
||||
total: 0,
|
||||
topMonitor: null,
|
||||
totalManualResolutions: 0,
|
||||
totalAutomaticResolutions: 0,
|
||||
latestIncidents: [],
|
||||
});
|
||||
|
||||
const [isLoadingSummary, setIsLoadingSummary] = useState(false);
|
||||
const [summaryError, setSummaryError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSummary = async () => {
|
||||
setIsLoadingSummary(true);
|
||||
setSummaryError(null);
|
||||
|
||||
try {
|
||||
const summaryData = await fetchIncidentSummary({
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
if (summaryData) {
|
||||
setSummary(summaryData);
|
||||
} else {
|
||||
setSummaryError(new Error("Failed to fetch summary"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching incident summary:", error);
|
||||
setSummaryError(error);
|
||||
} finally {
|
||||
setIsLoadingSummary(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSummary();
|
||||
}, [fetchIncidentSummary, updateTrigger]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm={4}
|
||||
>
|
||||
<ActiveIncidentsPanel
|
||||
totalCount={summary.totalActive || 0}
|
||||
isLoading={isLoadingSummary}
|
||||
error={summaryError}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm={4}
|
||||
>
|
||||
<LatestIncidentsPanel
|
||||
incidents={summary.latestIncidents || []}
|
||||
isLoading={isLoadingSummary}
|
||||
error={summaryError}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm={4}
|
||||
>
|
||||
<StatisticsPanel
|
||||
summary={summary}
|
||||
isLoading={isLoadingSummary}
|
||||
error={summaryError}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
IncidentsSummaryPanel.propTypes = {
|
||||
updateTrigger: PropTypes.bool.isRequired,
|
||||
};
|
||||
export default IncidentsSummaryPanel;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Skeleton } from "@mui/material";
|
||||
import SummaryCard from "../SummaryCard/index.jsx";
|
||||
|
||||
/**
|
||||
* Simple skeleton loader for summary panels
|
||||
* Reusable for ActiveIncidentsPanel, LatestIncidentsPanel, and StatisticsPanel
|
||||
*/
|
||||
const PanelSkeleton = () => {
|
||||
return (
|
||||
<SummaryCard sx={{ minHeight: "25vh" }}>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
</SummaryCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default PanelSkeleton;
|
||||
@@ -0,0 +1,153 @@
|
||||
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";
|
||||
|
||||
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 iconWrapperStyle = {
|
||||
px: theme.spacing(2),
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
"& svg path": {
|
||||
stroke: "currentColor",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
width: "100%",
|
||||
py: theme.spacing(0.5),
|
||||
"&:hover": { opacity: 0.8 },
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(1.5)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(1)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
...iconWrapperStyle,
|
||||
}}
|
||||
>
|
||||
<Monitors />
|
||||
</Box>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
alignItems="baseline"
|
||||
>
|
||||
<Typography variant="body1">
|
||||
{t("incidentsPage.incidentItemMonitor")}:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight={600}
|
||||
>
|
||||
{incident.monitorName || t("incidentsPage.unknownMonitor")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
marginTop={theme.spacing(1)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(3)}
|
||||
>
|
||||
<Box sx={{ ...iconWrapperStyle }}>
|
||||
<AverageResponseIcon />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
{t("incidentsPage.incidentItemStatus")}:
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<StatusLabel
|
||||
status={isActive ? "down" : "up"}
|
||||
text={isActive ? t("incidentsPage.active") : t("incidentsPage.resolved")}
|
||||
customStyles={{
|
||||
textTransform: "capitalize",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight={500}
|
||||
>
|
||||
{duration}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
IncidentItem.propTypes = {
|
||||
incident: PropTypes.shape({
|
||||
_id: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
monitorId: PropTypes.string,
|
||||
monitorName: PropTypes.string,
|
||||
status: PropTypes.bool,
|
||||
startTime: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
|
||||
endTime: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
|
||||
resolutionType: PropTypes.oneOf(["automatic", "manual"]),
|
||||
}),
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default IncidentItem;
|
||||
@@ -0,0 +1,99 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Stack, Typography, Divider } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
|
||||
import IncidentItem from "./IncidentItem.jsx";
|
||||
import SummaryCard from "../SummaryCard/index.jsx";
|
||||
|
||||
/**
|
||||
* LatestIncidentsPanel Component
|
||||
*
|
||||
* Displays a quick overview of recent incidents (both active and resolved).
|
||||
* Shows only essential information: monitor name, status, duration, and resolution type.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Array} props.incidents - Array of recent incidents
|
||||
* @param {boolean} props.isLoading - Loading state
|
||||
* @param {Object} props.error - Error object if any
|
||||
*/
|
||||
const LatestIncidentsPanel = ({ incidents = [], isLoading = false, error = null }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (isLoading) {
|
||||
return <PanelSkeleton />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<SummaryCard>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
minHeight: 200,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
color="error"
|
||||
align="center"
|
||||
>
|
||||
{t("incidentsPage.incidentsLatestPanelError")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</SummaryCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SummaryCard title={t("incidentsPage.incidentsLatestPanelTitle")}>
|
||||
{!incidents || incidents.length === 0 ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
textAlign="center"
|
||||
>
|
||||
{t("incidentsPage.incidentsLatestPanelEmpty")}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
{incidents.map((incident, index) => (
|
||||
<Box key={incident._id}>
|
||||
<IncidentItem incident={incident} />
|
||||
{index < incidents.length - 1 && <Divider sx={{ mt: theme.spacing(2) }} />}
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</SummaryCard>
|
||||
);
|
||||
};
|
||||
|
||||
LatestIncidentsPanel.propTypes = {
|
||||
incidents: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.string,
|
||||
monitorId: PropTypes.string,
|
||||
monitorName: PropTypes.string,
|
||||
status: PropTypes.bool,
|
||||
duration: PropTypes.string,
|
||||
resolutionType: PropTypes.oneOf(["automatic", "manual"]),
|
||||
})
|
||||
),
|
||||
isLoading: PropTypes.bool,
|
||||
error: PropTypes.object,
|
||||
onIncidentClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default LatestIncidentsPanel;
|
||||
@@ -0,0 +1,138 @@
|
||||
// Components
|
||||
import { Stack, Typography, Button, ButtonGroup } from "@mui/material";
|
||||
import Select from "@/Components/v1/Inputs/Select/index.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const OptionsHeader = ({
|
||||
shouldRender,
|
||||
selectedMonitor = 0,
|
||||
setSelectedMonitor,
|
||||
monitors,
|
||||
filter = "all",
|
||||
setFilter,
|
||||
dateRange = "all",
|
||||
setDateRange,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : [];
|
||||
|
||||
const filterOptions = [
|
||||
{ _id: "all", name: t("incidentsPage.incidentsOptionsHeaderFilterAll") },
|
||||
{ _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") },
|
||||
{ _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") },
|
||||
{ _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") },
|
||||
];
|
||||
|
||||
const stackStyles = {
|
||||
direction: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(6),
|
||||
};
|
||||
|
||||
if (!shouldRender) return;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack {...stackStyles}>
|
||||
<Typography
|
||||
display="inline-block"
|
||||
component="h1"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{t("incidentsOptionsHeader")}
|
||||
</Typography>
|
||||
<Select
|
||||
id="incidents-select-monitor"
|
||||
placeholder={t("incidentsOptionsPlaceholderAllServers")}
|
||||
value={selectedMonitor}
|
||||
onChange={(e) => setSelectedMonitor(e.target.value)}
|
||||
items={monitorNames}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
}}
|
||||
maxWidth={250}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack {...stackStyles}>
|
||||
<Typography
|
||||
display="inline-block"
|
||||
component="h1"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{t("incidentsOptionsHeaderFilterBy")}
|
||||
</Typography>
|
||||
<Select
|
||||
id="incidents-select-filter"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
items={filterOptions}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack {...stackStyles}>
|
||||
<Typography
|
||||
display="inline-block"
|
||||
component="h1"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{t("incidentsOptionsHeaderShow")}
|
||||
</Typography>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(dateRange === "hour").toString()}
|
||||
onClick={() => setDateRange("hour")}
|
||||
>
|
||||
{t("incidentsOptionsHeaderLastHour")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(dateRange === "day").toString()}
|
||||
onClick={() => setDateRange("day")}
|
||||
>
|
||||
{t("incidentsOptionsHeaderLastDay")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(dateRange === "week").toString()}
|
||||
onClick={() => setDateRange("week")}
|
||||
>
|
||||
{t("incidentsOptionsHeaderLastWeek")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(dateRange === "all").toString()}
|
||||
onClick={() => setDateRange("all")}
|
||||
>
|
||||
{t("incidentsOptionsHeaderFilterAll")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
OptionsHeader.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
selectedMonitor: PropTypes.string,
|
||||
setSelectedMonitor: PropTypes.func,
|
||||
monitors: PropTypes.object,
|
||||
filter: PropTypes.string,
|
||||
setFilter: PropTypes.func,
|
||||
dateRange: PropTypes.string,
|
||||
setDateRange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default OptionsHeader;
|
||||
@@ -0,0 +1,234 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Divider } from "@mui/material";
|
||||
import Clock from "@/assets/icons/maintenance.svg?react";
|
||||
import Incidents from "@/assets/icons/incidents.svg?react";
|
||||
import ResolutionItem from "@/assets/icons/interval-check.svg?react";
|
||||
import NotificationIcon from "@/assets/icons/notifications.svg?react";
|
||||
import SummaryCard from "../SummaryCard/index.jsx";
|
||||
|
||||
/**
|
||||
* StatisticsPanel Component
|
||||
*
|
||||
* Displays key metrics and statistics about incidents.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.statistics - Statistics data object
|
||||
* @param {number} props.statistics.total - Total incidents count
|
||||
* @param {number} props.statistics.totalTrend - Trend percentage for total (optional)
|
||||
* @param {string} props.statistics.mttr - Mean Time To Resolve (formatted string, e.g., "2h 15m")
|
||||
* @param {number} props.statistics.mttrTrend - Trend percentage for MTTR (optional)
|
||||
* @param {Object} props.statistics.topMonitor - Top monitor with most incidents
|
||||
* @param {string} props.statistics.topMonitor.name - Monitor name
|
||||
* @param {number} props.statistics.topMonitor.count - Incident count
|
||||
* @param {number} props.statistics.topMonitor.percentage - Percentage of total
|
||||
* @param {string} props.statistics.uptimeAffected - Total downtime (formatted string, e.g., "12h 45m")
|
||||
* @param {number} props.statistics.availability - Availability percentage
|
||||
* @param {boolean} props.isLoading - Loading state
|
||||
* @param {Object} props.error - Error object if any
|
||||
*/
|
||||
const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const totalResolutions =
|
||||
summary?.totalManualResolutions + summary?.totalAutomaticResolutions;
|
||||
const automaticPercentage =
|
||||
totalResolutions > 0
|
||||
? (summary?.totalAutomaticResolutions / totalResolutions) * 100
|
||||
: 0;
|
||||
|
||||
const iconWrapperStyle = {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
mx: theme.spacing(3),
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
"& svg path": {
|
||||
stroke: "currentColor",
|
||||
},
|
||||
};
|
||||
if (isLoading) {
|
||||
return <PanelSkeleton />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<SummaryCard>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
minHeight: 200,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
color="error"
|
||||
align="center"
|
||||
>
|
||||
{t("incidentsPage.incidentsStatisticsPanelError")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</SummaryCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SummaryCard title={t("incidentsPage.incidentsStatisticsPanelTitle")}>
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(3)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
...iconWrapperStyle,
|
||||
}}
|
||||
>
|
||||
<NotificationIcon />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{t("incidentsPage.totalIncidents")} : {summary.total || 0}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Box sx={iconWrapperStyle}>
|
||||
<Incidents />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{t("incidentsPage.mostAffectedMonitor")} :{" "}
|
||||
{summary.topMonitor?.monitorName || t("incidentsPage.unknownMonitor")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Box sx={iconWrapperStyle}>
|
||||
<Clock />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{t("incidentsPage.avgResolutionTime")} :{" "}
|
||||
{summary.avgResolutionTimeHours || 0} {t("incidentsPage.hours")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Divider />
|
||||
|
||||
<Box padding={theme.spacing(2)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
mb={theme.spacing(1.5)}
|
||||
>
|
||||
<Box sx={iconWrapperStyle}>
|
||||
<ResolutionItem />
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{t("incidentsPage.resolutions")}: {totalResolutions}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
bgcolor: theme.palette.accent.main,
|
||||
width: "100%",
|
||||
marginTop: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: `${automaticPercentage}%`,
|
||||
|
||||
bgcolor: theme.palette.warningSecondary.lowContrast,
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
paddingTop: theme.spacing(4),
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{summary.totalAutomaticResolutions > 0 && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={500}
|
||||
color={theme.palette.warningSecondary.contrastText}
|
||||
>
|
||||
{t("incidentsPage.automatic")} ({summary.totalAutomaticResolutions})
|
||||
</Typography>
|
||||
)}
|
||||
{summary.totalManualResolutions > 0 && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={500}
|
||||
color={theme.palette.accent.main}
|
||||
>
|
||||
{t("incidentsPage.manual")} ({summary.totalManualResolutions})
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</SummaryCard>
|
||||
);
|
||||
};
|
||||
|
||||
StatisticsPanel.propTypes = {
|
||||
summary: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
error: PropTypes.object,
|
||||
};
|
||||
|
||||
export default StatisticsPanel;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Paper } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Stack, Typography, Divider } from "@mui/material";
|
||||
const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
padding: theme.spacing(4),
|
||||
borderRadius: 3,
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
border: isHighPriority
|
||||
? `${theme.spacing(1.5)} solid ${theme.palette.error.lowContrast}`
|
||||
: `${theme.spacing(1)} solid ${theme.palette.divider}`,
|
||||
boxShadow: theme.palette.tertiary.cardShadow,
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{title && (
|
||||
<Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
padding: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 700,
|
||||
fontSize: theme.typography.h2.fontSize,
|
||||
|
||||
letterSpacing: theme.spacing(0.5),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
</Box>
|
||||
)}
|
||||
<Stack
|
||||
mt={theme.spacing(4)}
|
||||
paddingTop={theme.spacing(5)}
|
||||
gap={theme.spacing(4)}
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
SummaryCard.propTypes = {
|
||||
children: PropTypes.node,
|
||||
isHighPriority: PropTypes.bool,
|
||||
sx: PropTypes.object,
|
||||
title: PropTypes.string,
|
||||
};
|
||||
export default SummaryCard;
|
||||
@@ -0,0 +1,196 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { networkService } from "../../../../main.jsx";
|
||||
import { createToast } from "../../../../Utils/toastUtils.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
/**
|
||||
* Hook to fetch and manage incidents
|
||||
*
|
||||
* Provides multiple functions to fetch incidents with different filters.
|
||||
*
|
||||
* @returns {Object} {
|
||||
* incidents,
|
||||
* incidentsCount,
|
||||
* isLoading,
|
||||
* networkError,
|
||||
* fetchIncidents,
|
||||
* fetchActiveIncidents,
|
||||
* fetchResolvedIncidents,
|
||||
* fetchIncidentsByTeam,
|
||||
* fetchIncidentById,
|
||||
* resolveIncident,
|
||||
* fetchIncidentSummary
|
||||
* }
|
||||
*/
|
||||
const useFetchIncidents = () => {
|
||||
const [incidents, setIncidents] = useState(null);
|
||||
const [incidentsCount, setIncidentsCount] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
/**
|
||||
* Generic function to fetch incidents by team with custom config
|
||||
*
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {string} [config.monitorId] - Filter by monitor ID
|
||||
* @param {boolean} [config.status] - Filter by status (true=active, false=resolved)
|
||||
* @param {string} [config.resolutionType] - Filter by resolution type (automatic/manual)
|
||||
* @param {string} [config.sortOrder] - Sort order (asc/desc)
|
||||
* @param {string} [config.dateRange] - Date range filter
|
||||
* @param {number} [config.page] - Page number
|
||||
* @param {number} [config.rowsPerPage] - Rows per page
|
||||
*/
|
||||
const fetchIncidentsByTeam = async (config = {}) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setNetworkError(false);
|
||||
|
||||
const res = await networkService.getIncidentsByTeam(config);
|
||||
|
||||
setIncidents(res.data?.data?.incidents || []);
|
||||
setIncidentsCount(res.data?.data?.incidentsCount || 0);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
console.error(t("incidentsPage.errorFetchingIncidents"), error);
|
||||
createToast({ body: error.message || t("incidentsPage.errorFetchingIncidents") });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch active (open) incidents - Default function
|
||||
*
|
||||
* @param {Object} config - Additional configuration
|
||||
*/
|
||||
const fetchActiveIncidents = async (config = {}) => {
|
||||
await fetchIncidentsByTeam({
|
||||
status: true,
|
||||
sortOrder: "desc",
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch resolved incidents
|
||||
*
|
||||
* @param {Object} config - Additional configuration
|
||||
*/
|
||||
const fetchResolvedIncidents = async (config = {}) => {
|
||||
await fetchIncidentsByTeam({
|
||||
status: false,
|
||||
sortOrder: "desc",
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch incidents with custom filters (alias for fetchIncidentsByTeam)
|
||||
*
|
||||
* @param {Object} config - Configuration object
|
||||
*/
|
||||
const fetchIncidents = async (config = {}) => {
|
||||
await fetchIncidentsByTeam(config);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch a single incident by ID
|
||||
*
|
||||
* @param {string} incidentId - The ID of the incident to fetch
|
||||
* @returns {Promise<Object|null>} The incident object or null if not found
|
||||
*/
|
||||
const fetchIncidentById = async (incidentId) => {
|
||||
if (!incidentId) {
|
||||
console.error(t("incidentsPage.noIcidentIdProvided"));
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setNetworkError(false);
|
||||
|
||||
const res = await networkService.getIncidentById(incidentId);
|
||||
return res.data?.data || null;
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
console.error(t("incidentsPage.errorFetchingIncident"), error);
|
||||
createToast({ body: error.message || t("incidentsPage.errorFetchingIncident") });
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve an incident manually
|
||||
*
|
||||
* @param {string} incidentId - The ID of the incident to resolve
|
||||
* @param {Object} options - Resolution options
|
||||
* @param {string} [options.comment] - Optional comment about the resolution
|
||||
* @param {Function} [onSuccess] - Callback function to call on success
|
||||
* @param {Function} [onError] - Callback function to call on error
|
||||
*/
|
||||
const resolveIncident = async (incidentId, options = {}) => {
|
||||
if (!incidentId) {
|
||||
console.error(t("incidentsPage.noIcidentIdProvided"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setNetworkError(false);
|
||||
|
||||
const res = await networkService.resolveIncidentManually(incidentId, options);
|
||||
if (res.data?.success) {
|
||||
createToast({ body: t("incidentsPage.incidentResolvedSuccessfully") });
|
||||
} else {
|
||||
createToast({ body: t("incidentsPage.errorResolvingIncident") });
|
||||
}
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
console.error(t("incidentsPage.errorResolvingIncident"), error);
|
||||
const errorMessage =
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
t("incidentsPage.errorResolvingIncident");
|
||||
createToast({ body: errorMessage });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch incident summary
|
||||
*
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {number} [config.limit=10] - Number of latest incidents to return
|
||||
* @returns {Promise<Object|null>} The summary object or null if error
|
||||
*/
|
||||
const fetchIncidentSummary = useCallback(async (config = {}) => {
|
||||
try {
|
||||
const res = await networkService.getIncidentSummary(config);
|
||||
return res.data?.data || null;
|
||||
} catch (error) {
|
||||
console.error(t("incidentsPage.errorFetchingIncidentSummary"), error);
|
||||
createToast({
|
||||
body: error.message || t("incidentsPage.errorFetchingIncidentSummary"),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
incidents,
|
||||
incidentsCount,
|
||||
isLoading,
|
||||
networkError,
|
||||
fetchIncidents,
|
||||
fetchActiveIncidents,
|
||||
fetchResolvedIncidents,
|
||||
fetchIncidentsByTeam,
|
||||
fetchIncidentById,
|
||||
resolveIncident,
|
||||
fetchIncidentSummary,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFetchIncidents;
|
||||
@@ -0,0 +1,140 @@
|
||||
// Components
|
||||
import { Stack } from "@mui/material";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
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";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// Hooks
|
||||
import useFetchIncidents from "./hooks/useFetchIncidents.js";
|
||||
import { useFetchMonitorsByTeamId } from "../../../Hooks/v1/monitorHooks.js";
|
||||
|
||||
const Incidents2 = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const BREADCRUMBS = [
|
||||
{ name: t("incidentsPageTitle", "Incidents"), path: "/incidents" },
|
||||
];
|
||||
|
||||
const [selectedMonitor, setSelectedMonitor] = useState(undefined);
|
||||
const [filter, setFilter] = useState(undefined);
|
||||
const [dateRange, setDateRange] = useState("all");
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [monitorLookup, setMonitorLookup] = useState(undefined);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const handleUpdateTrigger = () => {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
};
|
||||
|
||||
const [monitors, , isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId(
|
||||
{}
|
||||
);
|
||||
|
||||
const {
|
||||
incidents,
|
||||
incidentsCount,
|
||||
isLoading: isLoadingIncidents,
|
||||
networkError: incidentsNetworkError,
|
||||
fetchIncidents,
|
||||
fetchActiveIncidents,
|
||||
fetchResolvedIncidents,
|
||||
resolveIncident,
|
||||
} = useFetchIncidents();
|
||||
|
||||
const networkError = monitorsNetworkError || incidentsNetworkError;
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [selectedMonitor, filter, dateRange]);
|
||||
|
||||
useEffect(() => {
|
||||
const config = {
|
||||
monitorId: selectedMonitor !== "0" ? selectedMonitor : undefined,
|
||||
sortOrder: "desc",
|
||||
dateRange,
|
||||
page,
|
||||
rowsPerPage,
|
||||
};
|
||||
|
||||
if (filter === "active") {
|
||||
fetchActiveIncidents(config);
|
||||
} else if (filter === "resolved") {
|
||||
fetchResolvedIncidents(config);
|
||||
} else {
|
||||
fetchIncidents(config);
|
||||
}
|
||||
}, [selectedMonitor, filter, dateRange, page, rowsPerPage, updateTrigger]); // Refetch when filters or pagination change
|
||||
|
||||
useEffect(() => {
|
||||
const lookup = monitors?.reduce((acc, monitor) => {
|
||||
acc[monitor._id] = {
|
||||
_id: monitor._id,
|
||||
name: monitor.name,
|
||||
type: monitor.type,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
setMonitorLookup(lookup);
|
||||
}, [monitors]);
|
||||
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<NetworkError />
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
const handleChangePage = (_, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
|
||||
<IncidentsSummaryPanel updateTrigger={updateTrigger} />
|
||||
|
||||
<OptionsHeader
|
||||
shouldRender={!isLoadingMonitors}
|
||||
monitors={monitorLookup}
|
||||
selectedMonitor={selectedMonitor}
|
||||
setSelectedMonitor={setSelectedMonitor}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
|
||||
<IncidentTable
|
||||
incidents={incidents || []}
|
||||
incidentsCount={incidentsCount || 0}
|
||||
isLoading={isLoadingIncidents}
|
||||
networkError={incidentsNetworkError}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
resolveIncident={resolveIncident}
|
||||
handleUpdateTrigger={handleUpdateTrigger}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Incidents2;
|
||||
@@ -30,7 +30,8 @@ import InfrastructureDetails from "../Pages/v1/Infrastructure/Details";
|
||||
import ServerUnreachable from "../Pages/v1/ServerUnreachable.jsx";
|
||||
|
||||
// Incidents
|
||||
import Incidents from "../Pages/v1/Incidents";
|
||||
import Checks from "../Pages/v1/Incidents";
|
||||
import Incidents2 from "../Pages/v1/Incidents2";
|
||||
|
||||
// Status pages
|
||||
import CreateStatus from "../Pages/v1/StatusPage/Create";
|
||||
@@ -137,9 +138,13 @@ const Routes = () => {
|
||||
path="infrastructure/:monitorId"
|
||||
element={<InfrastructureDetails />}
|
||||
/>
|
||||
<Route
|
||||
path="checks/:monitorId?"
|
||||
element={<Checks />}
|
||||
/>
|
||||
<Route
|
||||
path="incidents/:monitorId?"
|
||||
element={<Incidents />}
|
||||
element={<Incidents2 />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -1145,6 +1145,37 @@ class NetworkService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getIncidentsByTeam = async (config) => {
|
||||
const params = new URLSearchParams();
|
||||
if (config.monitorId) params.append("monitorId", config.monitorId);
|
||||
if (config.status !== undefined) params.append("status", config.status);
|
||||
if (config.resolutionType) params.append("resolutionType", config.resolutionType);
|
||||
if (config.sortOrder) params.append("sortOrder", config.sortOrder);
|
||||
if (config.dateRange) params.append("dateRange", config.dateRange);
|
||||
if (config.page) params.append("page", config.page);
|
||||
if (config.rowsPerPage) params.append("rowsPerPage", config.rowsPerPage);
|
||||
return this.axiosInstance.get(`/incidents/team?${params.toString()}`);
|
||||
};
|
||||
|
||||
getIncidentById = async (incidentId) => {
|
||||
return this.axiosInstance.get(`/incidents/${incidentId}`);
|
||||
};
|
||||
|
||||
resolveIncidentManually = async (incidentId, options = {}) => {
|
||||
const body = {};
|
||||
if (options.comment) body.comment = options.comment;
|
||||
return this.axiosInstance.put(`/incidents/${incidentId}/resolve`, body);
|
||||
};
|
||||
|
||||
getIncidentSummary = async (config = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (config.limit) params.append("limit", config.limit);
|
||||
const queryString = params.toString();
|
||||
return this.axiosInstance.get(
|
||||
`/incidents/team/summary${queryString ? `?${queryString}` : ""}`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default NetworkService;
|
||||
|
||||
@@ -163,6 +163,7 @@
|
||||
"incidentsOptionsHeader": "Incidents for:",
|
||||
"incidentsOptionsHeaderFilterBy": "Filter by:",
|
||||
"incidentsOptionsHeaderFilterAll": "All",
|
||||
"incidentsOptionsHeaderFilterActive": "Active",
|
||||
"incidentsOptionsHeaderFilterDown": "Down",
|
||||
"incidentsOptionsHeaderFilterCannotResolve": "Cannot Resolve",
|
||||
"incidentsOptionsHeaderShow": "Show:",
|
||||
@@ -463,7 +464,8 @@
|
||||
"team": "Team",
|
||||
"logOut": "Log out",
|
||||
"notifications": "Notifications",
|
||||
"logs": "Logs"
|
||||
"logs": "Logs",
|
||||
"checks": "Checks"
|
||||
},
|
||||
"settingsEmailUser": "Email user - Username for authentication, overrides email address if specified",
|
||||
"state": "State",
|
||||
@@ -472,6 +474,7 @@
|
||||
"commonSaving": "Saving...",
|
||||
"navControls": "Controls",
|
||||
"incidentsPageTitle": "Incidents",
|
||||
"checksPageTitle": "Checks",
|
||||
"passwordPanel": {
|
||||
"passwordChangedSuccess": "Your password was changed successfully.",
|
||||
"passwordInputIncorrect": "Your password input was incorrect.",
|
||||
@@ -1062,7 +1065,9 @@
|
||||
}
|
||||
},
|
||||
"incidentsPageActionResolveMonitor": "Resolve monitor incidents",
|
||||
"checksPageActionResolveMonitor": "Resolve monitor checks",
|
||||
"incidentsPageActionResolveAll": "Resolve all incidents",
|
||||
"checksPageActionResolveAllMonitor": "Resolve all checks",
|
||||
"matchMethodOptions": {
|
||||
"equal": "Equal",
|
||||
"equalPlaceholder": "success",
|
||||
@@ -1136,5 +1141,51 @@
|
||||
"packetsReceivedRate": "Packets Received Rate",
|
||||
"packetsSent": "Packets Sent",
|
||||
"rate": "Rate",
|
||||
"selectInterface": "Select Interface"
|
||||
"selectInterface": "Select Interface",
|
||||
"incidentsPage": {
|
||||
"title": "Incidents",
|
||||
"description": "Manage incidents for your monitors. You can resolve incidents here.",
|
||||
"resolved": "Resolved",
|
||||
"active": "Active",
|
||||
"startTime": "Start Time",
|
||||
"endTime": "End Time",
|
||||
"resolutionType": "Resolution Type",
|
||||
"unresolved": "Unresolved",
|
||||
"all": "All",
|
||||
"filterByMonitor": "Filter by monitor",
|
||||
"filterByStatus": "Filter by status",
|
||||
"filterByType": "Filter by type",
|
||||
"filterByDate": "Filter by date",
|
||||
"filterByPriority": "Filter by priority",
|
||||
"incidentsActivePanelError": "Error loading active incidents",
|
||||
"allSystemsAreOperational": "All Systems Are Operational",
|
||||
"incidentsActivePanelTitle": "Total Active Incidents",
|
||||
"incidentsLatestPanelError": "Error loading latest incidents",
|
||||
"incidentsLatestPanelTitle": "Latest Incidents",
|
||||
"incidentsLatestPanelEmpty": "No recent incidents",
|
||||
"incidentItemMonitor": "Monitor",
|
||||
"incidentItemStatus": "Status",
|
||||
"unknownMonitor": "Unknown Monitor",
|
||||
"incidentsStatisticsPanelError": "Error loading statistics",
|
||||
"incidentsStatisticsPanelTitle": "General Statistics",
|
||||
"totalIncidents": "Total Incidents",
|
||||
"mostAffectedMonitor": "Most Affected Monitor",
|
||||
"avgResolutionTime": "Avg. Resolution Time",
|
||||
"resolutions": "Resolutions",
|
||||
"automatic": "Automatic",
|
||||
"manual": "Manual",
|
||||
"incidentResolvedSuccessfully": "Incident resolved successfully",
|
||||
"incidentResolvedFailed": "Error resolving incident",
|
||||
"errorFetchingIncidents": "Error fetching incidents",
|
||||
"errorFetchingIncident": "Error fetching incident",
|
||||
"noIcidentIdProvided": "No incident ID provided",
|
||||
"errorFetchingIncidentSummary": "Error fetching incident summary",
|
||||
"incidentsOptionsHeaderFilterManual": "Manual",
|
||||
"incidentsOptionsHeaderFilterAll": "All",
|
||||
"incidentsOptionsHeaderFilterActive": "Active",
|
||||
"incidentsOptionsHeaderFilterResolved": "Resolved",
|
||||
"incidentsTableResolved": "Closed",
|
||||
"incidentsTableActionResolveManually": "Resolve Manually",
|
||||
"hours": "hours"
|
||||
}
|
||||
}
|
||||
@@ -77,13 +77,20 @@ class IncidentController extends BaseController {
|
||||
* @function getIncidentSummary
|
||||
* @param {Object} req - Express request object
|
||||
* @param {Object} req.query - Query parameters
|
||||
* @param {string} [req.query.dateRange] - Date range filter
|
||||
* @param {number} [req.query.limit=10] - Number of latest incidents to return
|
||||
* @param {Object} req.user - Current authenticated user (from JWT)
|
||||
* @param {string} req.user.teamId - User's team ID
|
||||
* @param {Object} res - Express response object
|
||||
* @returns {Promise<Object>} Success response with incidents summary
|
||||
* @returns {Promise<Object>} Success response with incidents summary containing:
|
||||
* - totalActive: Number of active incidents
|
||||
* - avgResolutionTimeHours: Average time to resolve incidents in hours
|
||||
* - topMonitor: Monitor with most incidents (monitorId, monitorName, incidentCount)
|
||||
* - total: Total number of incidents
|
||||
* - totalManualResolutions: Total incidents resolved manually
|
||||
* - totalAutomaticResolutions: Total incidents resolved automatically
|
||||
* - latestIncidents: Array of latest incidents created
|
||||
* @example
|
||||
* GET /incidents/summary?dateRange=week
|
||||
* GET /incidents/team/summary?limit=5
|
||||
* // Requires JWT authentication
|
||||
*/
|
||||
getIncidentSummary = this.asyncHandler(
|
||||
|
||||
@@ -185,13 +185,29 @@ class IncidentModule {
|
||||
...(statusBoolean !== undefined && { status: statusBoolean }),
|
||||
...(monitorId && { monitorId: new ObjectId(monitorId) }),
|
||||
...(resolutionType && { resolutionType }),
|
||||
...(dateRangeLookup[dateRange] && {
|
||||
startTime: {
|
||||
$gte: dateRangeLookup[dateRange],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
// Date range filter:
|
||||
// - Active incidents: always show (no date filter) - they're currently happening
|
||||
// - Resolved incidents: filter by endTime (when they were resolved)
|
||||
// - All incidents: show all active + resolved in the range
|
||||
if (dateRangeLookup[dateRange]) {
|
||||
const dateThreshold = dateRangeLookup[dateRange];
|
||||
if (statusBoolean === true) {
|
||||
// Active incidents: show all active incidents regardless of when they started
|
||||
// No date filter applied
|
||||
} else if (statusBoolean === false) {
|
||||
// Resolved incidents: only show if resolved in the range
|
||||
matchStage.endTime = { $gte: dateThreshold };
|
||||
} else {
|
||||
// All incidents: show all active + resolved incidents in the range
|
||||
matchStage.$or = [
|
||||
{ status: true }, // All active incidents
|
||||
{ status: false, endTime: { $gte: dateThreshold } }, // Resolved in range
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
const skip = page * rowsPerPage;
|
||||
@@ -199,6 +215,14 @@ class IncidentModule {
|
||||
const incidents = await this.Incident.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { startTime: sortOrder } },
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitors",
|
||||
localField: "monitorId",
|
||||
foreignField: "_id",
|
||||
as: "monitor",
|
||||
},
|
||||
},
|
||||
{
|
||||
$facet: {
|
||||
summary: [{ $count: "incidentsCount" }],
|
||||
@@ -211,7 +235,27 @@ class IncidentModule {
|
||||
$ifNull: [{ $arrayElemAt: ["$summary.incidentsCount", 0] }, 0],
|
||||
},
|
||||
incidents: {
|
||||
$ifNull: ["$incidents", []],
|
||||
$map: {
|
||||
input: { $ifNull: ["$incidents", []] },
|
||||
as: "incident",
|
||||
in: {
|
||||
$mergeObjects: [
|
||||
"$$incident",
|
||||
{
|
||||
monitorName: {
|
||||
$let: {
|
||||
vars: {
|
||||
monitor: { $arrayElemAt: ["$$incident.monitor", 0] },
|
||||
},
|
||||
in: {
|
||||
$ifNull: ["$$monitor.name", null],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -225,18 +269,14 @@ class IncidentModule {
|
||||
}
|
||||
};
|
||||
|
||||
getIncidentSummary = async ({ teamId, dateRange }) => {
|
||||
getIncidentSummary = async ({ teamId, limit = 10 }) => {
|
||||
try {
|
||||
const matchStage = {
|
||||
teamId: new ObjectId(teamId),
|
||||
...(dateRangeLookup[dateRange] && {
|
||||
startTime: {
|
||||
$gte: dateRangeLookup[dateRange],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const summary = await this.Incident.aggregate([
|
||||
// Get basic counts and resolution types
|
||||
const countsPipeline = [
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$group: {
|
||||
@@ -247,29 +287,135 @@ class IncidentModule {
|
||||
$cond: [{ $eq: ["$resolutionType", "manual"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
automaticResolutions: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$resolutionType", "automatic"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
const result = {
|
||||
total: 0,
|
||||
active: 0,
|
||||
resolved: 0,
|
||||
manual: 0,
|
||||
};
|
||||
const counts = await this.Incident.aggregate(countsPipeline);
|
||||
|
||||
summary.forEach((item) => {
|
||||
result.total += item.count;
|
||||
// Calculate totals
|
||||
let total = 0;
|
||||
let active = 0;
|
||||
let resolved = 0;
|
||||
let manual = 0;
|
||||
let automatic = 0;
|
||||
|
||||
counts.forEach((item) => {
|
||||
total += item.count;
|
||||
if (item._id === true) {
|
||||
result.active = item.count;
|
||||
active = item.count;
|
||||
}
|
||||
if (item._id === false) {
|
||||
result.resolved = item.count;
|
||||
resolved = item.count;
|
||||
}
|
||||
result.manual += item.manualResolutions;
|
||||
manual += item.manualResolutions;
|
||||
automatic += item.automaticResolutions;
|
||||
});
|
||||
|
||||
return result;
|
||||
// Calculate average resolution time (in milliseconds)
|
||||
const resolutionTimePipeline = [
|
||||
{ $match: { ...matchStage, status: false, endTime: { $exists: true, $ne: null } } },
|
||||
{
|
||||
$project: {
|
||||
resolutionTime: {
|
||||
$subtract: ["$endTime", "$startTime"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResolutionTime: { $avg: "$resolutionTime" },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const resolutionTimeResult = await this.Incident.aggregate(resolutionTimePipeline);
|
||||
const avgResolutionTimeMs = resolutionTimeResult[0]?.avgResolutionTime || 0;
|
||||
const avgResolutionTimeHours = avgResolutionTimeMs / (1000 * 60 * 60); // Convert to hours
|
||||
|
||||
// Get monitor with most incidents
|
||||
const monitorPipeline = [
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$group: {
|
||||
_id: "$monitorId",
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 1 },
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitors",
|
||||
localField: "_id",
|
||||
foreignField: "_id",
|
||||
as: "monitor",
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
monitorId: "$_id",
|
||||
count: 1,
|
||||
monitorName: { $arrayElemAt: ["$monitor.name", 0] },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const monitorResult = await this.Incident.aggregate(monitorPipeline);
|
||||
const topMonitor = monitorResult[0] || null;
|
||||
|
||||
// Get latest incidents
|
||||
const latestIncidentsPipeline = [
|
||||
{ $match: matchStage },
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{ $limit: parseInt(limit) },
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitors",
|
||||
localField: "monitorId",
|
||||
foreignField: "_id",
|
||||
as: "monitor",
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
monitorId: 1,
|
||||
monitorName: { $arrayElemAt: ["$monitor.name", 0] },
|
||||
status: 1,
|
||||
startTime: 1,
|
||||
endTime: 1,
|
||||
resolutionType: 1,
|
||||
message: 1,
|
||||
statusCode: 1,
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const latestIncidents = await this.Incident.aggregate(latestIncidentsPipeline);
|
||||
|
||||
return {
|
||||
totalActive: active,
|
||||
avgResolutionTimeHours: Math.round(avgResolutionTimeHours * 100) / 100, // Round to 2 decimal places
|
||||
topMonitor: topMonitor
|
||||
? {
|
||||
monitorId: topMonitor.monitorId,
|
||||
monitorName: topMonitor.monitorName,
|
||||
incidentCount: topMonitor.count,
|
||||
}
|
||||
: null,
|
||||
total: total,
|
||||
totalManualResolutions: manual,
|
||||
totalAutomaticResolutions: automatic,
|
||||
latestIncidents: latestIncidents,
|
||||
};
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getIncidentSummary";
|
||||
|
||||
@@ -215,11 +215,11 @@ class IncidentService {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const { dateRange } = query || {};
|
||||
const { limit } = query || {};
|
||||
|
||||
const summary = await this.db.incidentModule.getIncidentSummary({
|
||||
teamId,
|
||||
dateRange,
|
||||
limit,
|
||||
});
|
||||
|
||||
return summary;
|
||||
|
||||
Reference in new issue
Block a user