From 2b3c601a05ab2d0755ae45dbeb5277626f142954 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 8 Dec 2025 02:29:52 +0000 Subject: [PATCH] resolution card --- client/src/components/common/HeaderRange.tsx | 33 +++----- .../design-elements/ResolutionCard.tsx | 69 ++++++++++++++++ .../components/design-elements/StatBox.tsx | 16 +++- .../src/pages/incidents/IncidentDetails.tsx | 82 +++++++------------ client/src/pages/incidents/IncidentsPage.tsx | 11 ++- client/src/pages/uptime/UptimeDetails.tsx | 2 +- client/src/utils/MonitorUtils.ts | 15 +++- .../src/services/business/IncidentService.ts | 2 + 8 files changed, 150 insertions(+), 80 deletions(-) create mode 100644 client/src/components/design-elements/ResolutionCard.tsx diff --git a/client/src/components/common/HeaderRange.tsx b/client/src/components/common/HeaderRange.tsx index df5863b20..e32e039b4 100644 --- a/client/src/components/common/HeaderRange.tsx +++ b/client/src/components/common/HeaderRange.tsx @@ -7,10 +7,12 @@ export const HeaderRange = ({ range, setRange, loading, + all = false, }: { range: string; setRange: Function; loading: boolean; + all?: boolean; }) => { const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); @@ -26,34 +28,23 @@ export const HeaderRange = ({ orientation={isSmall ? "vertical" : "horizontal"} fullWidth={isSmall} variant="contained" - color={"primary"} + color="secondary" > - + )} + - - - diff --git a/client/src/components/design-elements/ResolutionCard.tsx b/client/src/components/design-elements/ResolutionCard.tsx new file mode 100644 index 000000000..f7644e501 --- /dev/null +++ b/client/src/components/design-elements/ResolutionCard.tsx @@ -0,0 +1,69 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { BaseBox } from "@/components/design-elements"; +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; + +export interface ResolutionCardProps { + resolved: boolean; + type?: string; + by?: string; + note?: string; + timestampLabel: string; // preformatted timestamp label + durationLabel?: string; // optional preformatted duration +} + +export const ResolutionCard = ({ + resolved, + type, + by, + note, + timestampLabel, + durationLabel, +}: ResolutionCardProps) => { + const theme = useTheme(); + const bg = resolved + ? theme.palette.success.lowContrast + : theme.palette.error.lowContrast; + + return ( + + + + Resolution + + + + + Status:{" "} + + {resolved ? "resolved" : "unresolved"} + + + {resolved && ( + <> + + Resolution type: {type || "N/A"} + + + Resolved by: {by || "N/A"} + + + )} + + {timestampLabel} + + {durationLabel && ( + + {durationLabel} + + )} + + Note: {note || "N/A"} + + + + ); +}; + +export default ResolutionCard; diff --git a/client/src/components/design-elements/StatBox.tsx b/client/src/components/design-elements/StatBox.tsx index 2fe1848d4..89326ca44 100644 --- a/client/src/components/design-elements/StatBox.tsx +++ b/client/src/components/design-elements/StatBox.tsx @@ -28,7 +28,7 @@ export const GradientBox: React.FC = ({ sx={{ padding: `${theme.spacing(4)} ${theme.spacing(8)}`, width: isSmall - ? `calc(50% - (1 * ${theme.spacing(8)} / 2))` + ? `100%` : `calc(25% - (3 * ${theme.spacing(8)} / 4))`, background: bg, @@ -46,6 +46,7 @@ type StatBoxProps = React.PropsWithChildren<{ palette?: PaletteKey; sx?: SxProps; tooltip?: string; + onClick?: () => void; }>; export const StatBox: React.FC = ({ @@ -55,13 +56,22 @@ export const StatBox: React.FC = ({ children, sx, tooltip, + onClick, }) => { const theme = useTheme(); const textColor = palette ? theme.palette[palette].contrastText : "inherit"; return ( - - + + diff --git a/client/src/pages/incidents/IncidentDetails.tsx b/client/src/pages/incidents/IncidentDetails.tsx index 32cd890a0..a06bd54a8 100644 --- a/client/src/pages/incidents/IncidentDetails.tsx +++ b/client/src/pages/incidents/IncidentDetails.tsx @@ -1,19 +1,23 @@ import { BasePage, StatBox } from "@/components/design-elements"; +import ResolutionCard from "@/components/design-elements/ResolutionCard"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import { useTheme } from "@mui/material/styles"; -import { useParams } from "react-router"; +import { useNavigate, useParams } from "react-router"; import { useGet } from "@/hooks/UseApi"; import type { ApiResponse } from "@/hooks/UseApi"; import type { IIncident } from "@/types/incident"; import { formatDateWithTz } from "@/utils/TimeUtils"; import { useAppSelector } from "@/hooks/AppHooks"; import prettyMilliseconds from "pretty-ms"; +import { getMonitorPath } from "@/utils/MonitorUtils"; +import type { MonitorType } from "@/types/monitor"; const IncidentDetailsPage = () => { const theme = useTheme(); const { id } = useParams<{ id: string }>(); const uiTimezone = useAppSelector((state: any) => state.ui.timezone); + const navigate = useNavigate(); const { response } = useGet>(`/incidents/${id}`); const incident = response?.data || null; @@ -39,6 +43,12 @@ const IncidentDetailsPage = () => { { + const type = incident.monitorId.type as MonitorType; + const path = getMonitorPath(type); + const id = (incident.monitorId as any)?._id; + if (id) navigate(`/${path}/${id}`); + }} /> { /> - - - Resolution - - - - Status: {incident.resolved ? "Resolved" : "Unresolved"} - - {incident.resolved && ( - <> - - Type: {incident.resolutionType || "N/A"} - - - By: {incident.resolvedBy?.email || "N/A"} - - - )} - - {incident.resolved && incident.endedAt - ? `Resolved at: ${formatDateWithTz( - incident.endedAt, - "ddd, MMMM D, YYYY, HH:mm A", - uiTimezone - )}` - : `Ongoing since: ${formatDateWithTz( - incident.startedAt, - "ddd, MMMM D, YYYY, HH:mm A", - uiTimezone - )}`} - - - - Duration:{" "} - {prettyMilliseconds(duration, { secondsDecimalDigits: 0 })} - - - Note: {incident.resolutionNote || "N/A"} - - + ); diff --git a/client/src/pages/incidents/IncidentsPage.tsx b/client/src/pages/incidents/IncidentsPage.tsx index 5b98e5996..c44edbcc1 100644 --- a/client/src/pages/incidents/IncidentsPage.tsx +++ b/client/src/pages/incidents/IncidentsPage.tsx @@ -21,7 +21,7 @@ const IncidentsPage = () => { const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); - const [range, setRange] = useState("2h"); + const [range, setRange] = useState("all"); const [resolved, setResolved] = useState(false); const { response, isValidating, refetch } = useGet< @@ -38,7 +38,7 @@ const IncidentsPage = () => { ); const incidents = response?.data?.incidents - ? [...response.data.incidents].reverse() + ? [...response.data.incidents] : []; const count = response?.data?.count || 0; @@ -103,7 +103,12 @@ const IncidentsPage = () => { Unresolved - + { refetch={refetch} path="/uptime" /> - + { + const pathMap: Record = { + http: "uptime", + https: "uptime", + port: "uptime", + ping: "uptime", + infrastructure: "infrastructure", + pagespeed: "pagespeed", + }; + return pathMap[type]; +}; + export const getStatusPalette = (status: MonitorStatus): PaletteKey => { const paletteMap: Record = { up: "success", diff --git a/server/src/services/business/IncidentService.ts b/server/src/services/business/IncidentService.ts index 0b70a2fa1..788d7018d 100644 --- a/server/src/services/business/IncidentService.ts +++ b/server/src/services/business/IncidentService.ts @@ -62,6 +62,8 @@ class IncidentService implements IIncidentService { private getStartDate(range: string): Date { const now = new Date(); switch (range) { + case "all": + return new Date(0); case "2h": return new Date(now.getTime() - 2 * 60 * 60 * 1000); case "24h":