resolution card
This commit is contained in:
1 parent
b4135e26ce
commit
2b3c601a05
8 files changed
+150
-80
No files matched your search
@@ -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) }}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in new issue
Block a user