resolution card

This commit is contained in:
Alex Holliday committed 2025-12-08 02:29:52 +00:00
1 parent b4135e26ce
commit 2b3c601a05
8 files changed
+150 -80

No files matched your search

+12 -21
View File
@@ -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"
>
<Button
color={range === "2h" ? "secondary" : "primary"}
onClick={() => setRange("2h")}
loading={loading}
>
{all && (
<Button onClick={() => setRange("all")} loading={loading}>
All
</Button>
)}
<Button onClick={() => setRange("2h")} loading={loading}>
Recent
</Button>
<Button
color={range === "24h" ? "secondary" : "primary"}
onClick={() => setRange("24h")}
loading={loading}
>
<Button onClick={() => setRange("24h")} loading={loading}>
Day
</Button>
<Button
color={range === "7d" ? "secondary" : "primary"}
onClick={() => setRange("7d")}
loading={loading}
>
<Button onClick={() => setRange("7d")} loading={loading}>
7 days
</Button>
<Button
color={range === "30d" ? "secondary" : "primary"}
onClick={() => setRange("30d")}
loading={loading}
>
<Button onClick={() => setRange("30d")} loading={loading}>
30 days
</Button>
</ButtonGroup>
@@ -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 (
<BaseBox sx={{ maxWidth: 560 }}>
<Box sx={{ bgcolor: bg, p: 6 }}>
<Typography variant="h6" color="textPrimary">
Resolution
</Typography>
</Box>
<Stack spacing={3} padding={6}>
<Typography variant="body2" color="textPrimary">
Status:{" "}
<span style={{ color: bg }}>
{resolved ? "resolved" : "unresolved"}
</span>
</Typography>
{resolved && (
<>
<Typography variant="body2" color="textPrimary">
Resolution type: {type || "N/A"}
</Typography>
<Typography variant="body2" color="textPrimary">
Resolved by: {by || "N/A"}
</Typography>
</>
)}
<Typography variant="body2" color="textPrimary">
{timestampLabel}
</Typography>
{durationLabel && (
<Typography variant="body2" color="textSecondary">
{durationLabel}
</Typography>
)}
<Typography variant="body2" color="textSecondary">
Note: {note || "N/A"}
</Typography>
</Stack>
</BaseBox>
);
};
export default ResolutionCard;
@@ -28,7 +28,7 @@ export const GradientBox: React.FC<GradientBox> = ({
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<StatBoxProps> = ({
@@ -55,13 +56,22 @@ export const StatBox: React.FC<StatBoxProps> = ({
children,
sx,
tooltip,
onClick,
}) => {
const theme = useTheme();
const textColor = palette ? theme.palette[palette].contrastText : "inherit";
return (
<GradientBox palette={palette} sx={sx}>
<Stack>
<GradientBox
palette={palette}
sx={{
...(sx as object),
...(onClick
? { cursor: "pointer", '&:hover': { opacity: 0.95 } }
: {}),
}}
>
<Stack onClick={onClick}>
<Box
sx={{ display: "flex", alignItems: "center", gap: theme.spacing(2) }}
>
+31 -51
View File
@@ -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<ApiResponse<IIncident>>(`/incidents/${id}`);
const incident = response?.data || null;
@@ -39,6 +43,12 @@ const IncidentDetailsPage = () => {
<StatBox
title="Monitor"
subtitle={incident.monitorId?.name || "N/A"}
onClick={() => {
const type = incident.monitorId.type as MonitorType;
const path = getMonitorPath(type);
const id = (incident.monitorId as any)?._id;
if (id) navigate(`/${path}/${id}`);
}}
/>
<StatBox
title="Start"
@@ -62,56 +72,26 @@ const IncidentDetailsPage = () => {
/>
</Stack>
<Stack spacing={theme.spacing(4)}>
<Typography variant="h6" color="textPrimary">
Resolution
</Typography>
<Stack
direction={"column"}
spacing={theme.spacing(4)}
sx={{
p: theme.spacing(4),
borderRadius: 2,
bgcolor: incident.resolved
? (theme.palette.success.lowContrast as any)
: (theme.palette.error.lowContrast as any),
}}
>
<Typography variant="body2" color="textPrimary">
Status: {incident.resolved ? "Resolved" : "Unresolved"}
</Typography>
{incident.resolved && (
<>
<Typography variant="body2" color="textPrimary">
Type: {incident.resolutionType || "N/A"}
</Typography>
<Typography variant="body2" color="textPrimary">
By: {incident.resolvedBy?.email || "N/A"}
</Typography>
</>
)}
<Typography variant="body2" color="textPrimary">
{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
)}`}
</Typography>
</Stack>
<Typography variant="body2" color="textSecondary">
Duration:{" "}
{prettyMilliseconds(duration, { secondsDecimalDigits: 0 })}
</Typography>
<Typography variant="body2" color="textSecondary">
Note: {incident.resolutionNote || "N/A"}
</Typography>
</Stack>
<ResolutionCard
resolved={incident.resolved}
type={incident.resolutionType}
by={incident.resolvedBy?.email}
note={incident.resolutionNote}
timestampLabel={
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
)}`
}
durationLabel={`Duration: ${prettyMilliseconds(duration, { secondsDecimalDigits: 0 })}`}
/>
</Stack>
</BasePage>
);
+8 -3
View File
@@ -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<boolean | null>(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 = () => {
<MenuItem value={"unresolved"}>Unresolved</MenuItem>
</Select>
</Stack>
<HeaderRange range={range} setRange={setRange} loading={isValidating} />
<HeaderRange
range={range}
setRange={setRange}
loading={isValidating}
all={true}
/>
</Stack>
<IncidentTable
incidents={incidents}
+1 -1
View File
@@ -110,7 +110,7 @@ const UptimeDetailsPage = () => {
refetch={refetch}
path="/uptime"
/>
<Stack direction="row" gap={theme.spacing(8)}>
<Stack direction={isSmall ? "column" : "row"} gap={theme.spacing(8)}>
<StatBox
palette={palette}
title="Active for"
+14 -1
View File
@@ -1,7 +1,20 @@
import type { MonitorStatus } from "@/types/monitor";
import type { IMonitor } from "@/types/monitor";
import type { IMonitor, MonitorType } from "@/types/monitor";
import type { PaletteKey } from "@/theme/theme";
import type { ValueType } from "@/components/design-elements/StatusLabel";
export const getMonitorPath = (type: MonitorType): string => {
const pathMap: Record<MonitorType, string> = {
http: "uptime",
https: "uptime",
port: "uptime",
ping: "uptime",
infrastructure: "infrastructure",
pagespeed: "pagespeed",
};
return pathMap[type];
};
export const getStatusPalette = (status: MonitorStatus): PaletteKey => {
const paletteMap: Record<MonitorStatus, PaletteKey> = {
up: "success",
@@ -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":