Merge pull request #3638 from bluewave-labs/feat/status-code-down-label
feat(checks): render status code 5000 as "Down" with subtype tooltip
This commit is contained in:
10 files changed
+217
-80
No files matched your search
@@ -0,0 +1,40 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
formatStatusCode,
|
||||
getStatusCodeTooltip,
|
||||
getStatusCodeValueType,
|
||||
} from "@/Utils/statusCode";
|
||||
import { ValueLabel } from "./StatusLabel";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
export type StatusCodeLabelProps = {
|
||||
statusCode: number | null | undefined;
|
||||
message?: string | null;
|
||||
fallback?: string;
|
||||
};
|
||||
|
||||
export const StatusCodeLabel = ({
|
||||
statusCode,
|
||||
message,
|
||||
fallback = "N/A",
|
||||
}: StatusCodeLabelProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (!statusCode) {
|
||||
return <>{fallback}</>;
|
||||
}
|
||||
|
||||
const text = formatStatusCode(statusCode, t);
|
||||
const tooltip = getStatusCodeTooltip(statusCode, message, t);
|
||||
const body = (
|
||||
<ValueLabel
|
||||
value={getStatusCodeValueType(statusCode)}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
if (!tooltip) return body;
|
||||
return (
|
||||
<Tooltip title={tooltip}>
|
||||
<span>{body}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -25,3 +25,4 @@ export * from "./TextLink";
|
||||
export * from "./NoticeBanner";
|
||||
export * from "./OfflineBanner";
|
||||
export * from "./Avatar";
|
||||
export * from "./StatusCodeLabel";
|
||||
@@ -3,6 +3,8 @@ import { Box, Typography, Stack, useTheme, GlobalStyles } from "@mui/material";
|
||||
import Map, { Marker, Popup } from "react-map-gl/maplibre";
|
||||
import type { MapRef } from "react-map-gl/maplibre";
|
||||
import type { FlatGeoCheck } from "@/Types/GeoCheck";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatStatusCode } from "@/Utils/statusCode";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
interface GeoChecksMapProps {
|
||||
@@ -14,6 +16,7 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
|
||||
const [selectedCheck, setSelectedCheck] = useState<FlatGeoCheck | null>(null);
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
const { t } = useTranslation();
|
||||
|
||||
const mapPopupStyles = (
|
||||
<GlobalStyles
|
||||
@@ -174,7 +177,7 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
|
||||
>
|
||||
Status Code:
|
||||
</Typography>{" "}
|
||||
{selectedCheck.statusCode}
|
||||
{formatStatusCode(selectedCheck.statusCode, t)}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<Typography
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Table, Pagination, ValueLabel, StatusLabel } from "@/Components/design-elements";
|
||||
import {
|
||||
Table,
|
||||
Pagination,
|
||||
StatusLabel,
|
||||
StatusCodeLabel,
|
||||
} from "@/Components/design-elements";
|
||||
import Box from "@mui/material/Box";
|
||||
import type { Header } from "@/Components/design-elements/Table";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
import type { Check } from "@/Types/Check";
|
||||
@@ -29,7 +33,7 @@ export const ChecksTable = ({
|
||||
const { t } = useTranslation();
|
||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||
|
||||
const getHeaders = (t: Function, uiTimezone: string) => {
|
||||
const getHeaders = () => {
|
||||
const headers: Header<Check>[] = [
|
||||
{
|
||||
id: "monitorName",
|
||||
@@ -63,13 +67,10 @@ export const ChecksTable = ({
|
||||
id: "statusCode",
|
||||
content: t("pages.checks.table.headers.statusCode"),
|
||||
render: (row) => {
|
||||
const code = row.statusCode;
|
||||
if (!code) return "N/A";
|
||||
const value = code < 300 ? "positive" : code < 400 ? "neutral" : "negative";
|
||||
return (
|
||||
<ValueLabel
|
||||
value={value}
|
||||
text={String(code)}
|
||||
<StatusCodeLabel
|
||||
statusCode={row.statusCode}
|
||||
message={row.message}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -85,7 +86,7 @@ export const ChecksTable = ({
|
||||
return headers;
|
||||
};
|
||||
|
||||
const headers = getHeaders(t, uiTimezone);
|
||||
const headers = getHeaders();
|
||||
|
||||
const handlePageChange = (
|
||||
_e: React.MouseEvent<HTMLButtonElement> | null,
|
||||
|
||||
@@ -2,7 +2,7 @@ import Stack from "@mui/material/Stack";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { BaseBox, ValueLabel } from "@/Components/design-elements";
|
||||
import { BaseBox, ValueLabel, StatusCodeLabel } from "@/Components/design-elements";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -46,6 +46,7 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
||||
if (!incident) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
@@ -123,7 +124,12 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
||||
<Cell>{t("pages.incidents.dialog.details.statusCode")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Cell>{incident.statusCode ?? "N/A"}</Cell>
|
||||
<Cell>
|
||||
<StatusCodeLabel
|
||||
statusCode={incident.statusCode}
|
||||
message={incident.message}
|
||||
/>
|
||||
</Cell>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.downtime")}</Cell>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Table } from "@/Components/design-elements";
|
||||
import { Table, ValueLabel, StatusCodeLabel } from "@/Components/design-elements";
|
||||
import { Pagination } from "@/Components/design-elements/Table";
|
||||
import type { Header } from "@/Components/design-elements/Table";
|
||||
import { ValueLabel } from "@/Components/design-elements";
|
||||
import type { ValueType } from "@/Components/design-elements/StatusLabel";
|
||||
import { ActionsMenu } from "@/Components/actions-menu";
|
||||
import type { Incident } from "@/Types/Incident";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
@@ -136,13 +134,12 @@ export const IncidentsTable = ({
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
color:
|
||||
row.resolutionType === "manual"
|
||||
? theme.palette.warning.main
|
||||
: theme.palette.success.main,
|
||||
}}
|
||||
color={
|
||||
row.resolutionType === "manual"
|
||||
? theme.palette.warning.main
|
||||
: theme.palette.success.main
|
||||
}
|
||||
textTransform={"capitalize"}
|
||||
>
|
||||
{row.resolutionType}
|
||||
</Typography>
|
||||
@@ -155,15 +152,10 @@ export const IncidentsTable = ({
|
||||
id: "statusCode",
|
||||
content: t("pages.incidents.table.headers.statusCode"),
|
||||
render: (row) => {
|
||||
const code = row.statusCode;
|
||||
if (!code) return "N/A";
|
||||
let value: ValueType = "neutral";
|
||||
if (code < 300) value = "positive";
|
||||
else if (code >= 400) value = "negative";
|
||||
return (
|
||||
<ValueLabel
|
||||
value={value}
|
||||
text={String(code)}
|
||||
<StatusCodeLabel
|
||||
statusCode={row.statusCode}
|
||||
message={row.message}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Table, Pagination, StatusLabel } from "@/Components/design-elements";
|
||||
import {
|
||||
Table,
|
||||
Pagination,
|
||||
StatusLabel,
|
||||
StatusCodeLabel,
|
||||
} from "@/Components/design-elements";
|
||||
import Box from "@mui/material/Box";
|
||||
import type { Header } from "@/Components/design-elements";
|
||||
import type { Check } from "@/Types/Check";
|
||||
@@ -9,7 +14,25 @@ import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const getHeaders = (t: Function, uiTimezone: string) => {
|
||||
export const ChecksTable = ({
|
||||
checks,
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
rowsPerPage,
|
||||
setRowsPerPage,
|
||||
}: {
|
||||
checks: Check[];
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
rowsPerPage: number;
|
||||
setRowsPerPage: (rowsPerPage: number) => void;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||
|
||||
const headers: Header<Check>[] = [
|
||||
{
|
||||
id: "status",
|
||||
@@ -36,32 +59,15 @@ const getHeaders = (t: Function, uiTimezone: string) => {
|
||||
id: "statusCode",
|
||||
content: t("pages.checks.table.headers.statusCode"),
|
||||
render: (row) => {
|
||||
return row.statusCode || "N/A";
|
||||
return (
|
||||
<StatusCodeLabel
|
||||
statusCode={row.statusCode}
|
||||
message={row.message}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const ChecksTable = ({
|
||||
checks,
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
rowsPerPage,
|
||||
setRowsPerPage,
|
||||
}: {
|
||||
checks: Check[];
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
rowsPerPage: number;
|
||||
setRowsPerPage: (rowsPerPage: number) => void;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||
const headers = getHeaders(t, uiTimezone);
|
||||
|
||||
const handlePageChange = (
|
||||
_e: React.MouseEvent<HTMLButtonElement> | null,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Table, Pagination, StatusLabel } from "@/Components/design-elements";
|
||||
import {
|
||||
Table,
|
||||
Pagination,
|
||||
StatusLabel,
|
||||
StatusCodeLabel,
|
||||
} from "@/Components/design-elements";
|
||||
import Box from "@mui/material/Box";
|
||||
import type { Header } from "@/Components/design-elements";
|
||||
import type { FlatGeoCheck } from "@/Types/GeoCheck";
|
||||
@@ -8,7 +13,24 @@ import type { RootState } from "@/Types/state";
|
||||
import { useSelector } from "react-redux";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
|
||||
const getHeaders = (t: Function, uiTimezone: string) => {
|
||||
export const GeoChecksTable = ({
|
||||
geoChecks,
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
rowsPerPage,
|
||||
setRowsPerPage,
|
||||
}: {
|
||||
geoChecks: FlatGeoCheck[];
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
rowsPerPage: number;
|
||||
setRowsPerPage: (rowsPerPage: number) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||
|
||||
const headers: Header<FlatGeoCheck>[] = [
|
||||
{
|
||||
id: "status",
|
||||
@@ -29,7 +51,7 @@ const getHeaders = (t: Function, uiTimezone: string) => {
|
||||
id: "statusCode",
|
||||
content: t("pages.checks.table.headers.statusCode"),
|
||||
render: (row) => {
|
||||
return row.statusCode || "N/A";
|
||||
return <StatusCodeLabel statusCode={row.statusCode} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -51,27 +73,6 @@ const getHeaders = (t: Function, uiTimezone: string) => {
|
||||
},
|
||||
},
|
||||
];
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const GeoChecksTable = ({
|
||||
geoChecks,
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
rowsPerPage,
|
||||
setRowsPerPage,
|
||||
}: {
|
||||
geoChecks: FlatGeoCheck[];
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
rowsPerPage: number;
|
||||
setRowsPerPage: (rowsPerPage: number) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||
const headers = getHeaders(t, uiTimezone);
|
||||
|
||||
const handlePageChange = (
|
||||
_e: React.MouseEvent<HTMLButtonElement> | null,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ValueType } from "@/Components/design-elements/StatusLabel";
|
||||
|
||||
export const NETWORK_ERROR_CODE = 5000;
|
||||
|
||||
export const NETWORK_ERROR_SUBTYPES = [
|
||||
"timeout",
|
||||
"dns",
|
||||
"refused",
|
||||
"tls",
|
||||
"reset",
|
||||
"unknown",
|
||||
] as const;
|
||||
export type NetworkErrorSubtype = (typeof NETWORK_ERROR_SUBTYPES)[number];
|
||||
|
||||
type TranslateFn = (key: string) => string;
|
||||
|
||||
export const isNetworkError = (code?: number | null): boolean =>
|
||||
code === NETWORK_ERROR_CODE;
|
||||
|
||||
const ERROR_CODE_PATTERN =
|
||||
/\b(E[A-Z_]{2,}|CERT_[A-Z_]+|UNABLE_TO_VERIFY[A-Z_]*|DEPTH_ZERO_SELF_SIGNED[A-Z_]*|SELF_SIGNED[A-Z_]*)\b/;
|
||||
|
||||
export const extractErrorCode = (message?: string | null): string | null => {
|
||||
if (!message) return null;
|
||||
const match = message.match(ERROR_CODE_PATTERN);
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
export const getNetworkErrorSubtype = (message?: string | null): NetworkErrorSubtype => {
|
||||
if (!message) return "unknown";
|
||||
const m = message.toUpperCase();
|
||||
if (m.includes("ETIMEDOUT") || m.includes("TIMEOUT") || m.includes("ESOCKETTIMEDOUT"))
|
||||
return "timeout";
|
||||
if (m.includes("ENOTFOUND") || m.includes("EAI_AGAIN") || m.includes("DNS"))
|
||||
return "dns";
|
||||
if (m.includes("ECONNREFUSED")) return "refused";
|
||||
if (
|
||||
m.includes("CERT_") ||
|
||||
m.includes("UNABLE_TO_VERIFY") ||
|
||||
m.includes("DEPTH_ZERO_SELF_SIGNED") ||
|
||||
m.includes("SELF_SIGNED") ||
|
||||
m.includes("TLS") ||
|
||||
m.includes("SSL")
|
||||
)
|
||||
return "tls";
|
||||
if (m.includes("ECONNRESET")) return "reset";
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
export const formatStatusCode = (
|
||||
code: number | null | undefined,
|
||||
t: TranslateFn
|
||||
): string => {
|
||||
if (isNetworkError(code)) return t("common.statusCode.down");
|
||||
return String(code);
|
||||
};
|
||||
|
||||
export const getStatusCodeTooltip = (
|
||||
code: number | null | undefined,
|
||||
message: string | null | undefined,
|
||||
t: TranslateFn
|
||||
): string | null => {
|
||||
if (!isNetworkError(code)) return null;
|
||||
const description = t(
|
||||
`common.statusCode.networkError.${getNetworkErrorSubtype(message)}`
|
||||
);
|
||||
const errorCode = extractErrorCode(message);
|
||||
return errorCode ? `${description} (${errorCode})` : description;
|
||||
};
|
||||
|
||||
export const getStatusCodeValueType = (code: number): ValueType => {
|
||||
if (isNetworkError(code)) return "negative";
|
||||
if (code < 300) return "positive";
|
||||
if (code < 400) return "neutral";
|
||||
return "negative";
|
||||
};
|
||||
@@ -14,6 +14,17 @@
|
||||
}
|
||||
},
|
||||
"appName": "Checkmate",
|
||||
"statusCode": {
|
||||
"down": "Down",
|
||||
"networkError": {
|
||||
"timeout": "The server didn't respond in time (request timed out).",
|
||||
"dns": "Couldn't resolve the hostname (DNS lookup failed).",
|
||||
"refused": "The server refused the connection (port closed or service not listening).",
|
||||
"tls": "TLS or SSL handshake failed (certificate or encryption issue).",
|
||||
"reset": "The connection was reset before a response was received.",
|
||||
"unknown": "No HTTP response was received from the target."
|
||||
}
|
||||
},
|
||||
"breadcrumbs": {
|
||||
"details": "Details",
|
||||
"home": "Home"
|
||||
|
||||
Reference in new issue
Block a user