mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-20 08:28:48 -05:00
Merge pull request #3215 from bluewave-labs/feat/v2-checks
feat: v2 checks
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { ToggleButtonGroup, ToggleButton } from "@/Components/v2/inputs";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
|
||||
interface MonitorTimeFrameHeaderProps {
|
||||
isLoading?: boolean;
|
||||
@@ -17,7 +21,8 @@ export const HeaderTimeRange = ({
|
||||
setDateRange,
|
||||
}: MonitorTimeFrameHeaderProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const handleChange = (
|
||||
_event: React.MouseEvent<HTMLElement>,
|
||||
newValue: string | null
|
||||
@@ -32,34 +37,24 @@ export const HeaderTimeRange = ({
|
||||
if (hasDateRange) {
|
||||
timeFramePicker = (
|
||||
<ToggleButtonGroup
|
||||
orientation={isSmallScreen ? "vertical" : "horizontal"}
|
||||
value={dateRange}
|
||||
exclusive
|
||||
onChange={handleChange}
|
||||
size="small"
|
||||
fullWidth={isSmallScreen}
|
||||
>
|
||||
<ToggleButton
|
||||
disabled={isLoading}
|
||||
value="recent"
|
||||
>
|
||||
Recent
|
||||
<ToggleButton value="recent">
|
||||
{t("components.headerTimeRange.labels.recent")}
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
disabled={isLoading}
|
||||
value="day"
|
||||
>
|
||||
Day
|
||||
<ToggleButton value="day">
|
||||
{t("components.headerTimeRange.labels.day")}
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
disabled={isLoading}
|
||||
value="week"
|
||||
>
|
||||
Week
|
||||
<ToggleButton value="week">
|
||||
{t("components.headerTimeRange.labels.week")}
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
disabled={isLoading}
|
||||
value="month"
|
||||
>
|
||||
Month
|
||||
<ToggleButton value="month">
|
||||
{t("components.headerTimeRange.labels.month")}
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
);
|
||||
@@ -67,21 +62,19 @@ export const HeaderTimeRange = ({
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Box
|
||||
width={16}
|
||||
height={16}
|
||||
>
|
||||
{isLoading && <CircularProgress size={16} />}
|
||||
</Box>
|
||||
<Typography variant="body2">
|
||||
Showing statistics for past{" "}
|
||||
{dateRange === "recent"
|
||||
? "2 hours"
|
||||
: dateRange === "day"
|
||||
? "24 hours"
|
||||
: dateRange === "week"
|
||||
? "7 days"
|
||||
: "30 days"}
|
||||
.
|
||||
{t(`components.headerTimeRange.description.${dateRange}`)}
|
||||
</Typography>
|
||||
{timeFramePicker}
|
||||
</Stack>
|
||||
|
||||
@@ -4,12 +4,16 @@ import Box from "@mui/material/Box";
|
||||
import { BaseBox } from "@/Components/v2/design-elements";
|
||||
import Background from "@/assets/Images/background-grid.svg?react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SxProps } from "@mui/material";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
type StatusBoxProps = React.PropsWithChildren<{ children: React.ReactNode }>;
|
||||
type StatusBoxProps = React.PropsWithChildren<{
|
||||
children: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
}>;
|
||||
|
||||
export const BGBox = ({ children }: StatusBoxProps) => {
|
||||
export const BGBox = ({ children, sx }: StatusBoxProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BaseBox
|
||||
@@ -18,6 +22,7 @@ export const BGBox = ({ children }: StatusBoxProps) => {
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
padding: theme.spacing(4),
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -36,14 +41,16 @@ const StatusBox = ({
|
||||
label,
|
||||
n,
|
||||
color,
|
||||
sx,
|
||||
}: {
|
||||
label: string;
|
||||
n: number;
|
||||
color: string | undefined;
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BGBox>
|
||||
<BGBox sx={sx}>
|
||||
<Stack spacing={theme.spacing(4)}>
|
||||
<Typography
|
||||
variant={"h2"}
|
||||
@@ -98,6 +105,39 @@ export const PausedStatusBox = ({ n }: { n: number }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const TotalChecksBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("pages.common.monitors.status.total")}
|
||||
n={n}
|
||||
color={theme.palette.primary.light}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const DownChecksBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("pages.common.monitors.status.down")}
|
||||
n={n}
|
||||
color={theme.palette.error.light}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const UpChecksBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("pages.common.monitors.status.up")}
|
||||
n={n}
|
||||
color={theme.palette.success.light}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InitializingStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -21,7 +21,7 @@ export const useGet = <T>(
|
||||
axiosConfig?: AxiosRequestConfig,
|
||||
swrConfig?: SWRConfiguration
|
||||
) => {
|
||||
const { data, error, isLoading, mutate } = useSWR<ApiResponse<T>>(
|
||||
const { data, error, isLoading, isValidating, mutate } = useSWR<ApiResponse<T>>(
|
||||
url,
|
||||
(url: string) => fetcher<T>(url, axiosConfig),
|
||||
swrConfig
|
||||
@@ -30,6 +30,7 @@ export const useGet = <T>(
|
||||
return {
|
||||
data: data?.data ?? null,
|
||||
isLoading,
|
||||
isValidating,
|
||||
error,
|
||||
refetch: mutate,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
Table,
|
||||
Pagination,
|
||||
ValueLabel,
|
||||
StatusLabel,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import Box from "@mui/material/Box";
|
||||
import type { Header } from "@/Components/v2/design-elements/Table";
|
||||
import type { Monitor, MonitorStatus } from "@/Types/Monitor";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
import { useNavigate } from "react-router";
|
||||
import type { Check } from "@/Types/Check";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
export const ChecksTable = ({
|
||||
monitors,
|
||||
checks,
|
||||
checksCount,
|
||||
page,
|
||||
setPage,
|
||||
rowsPerPage,
|
||||
setRowsPerPage,
|
||||
}: {
|
||||
monitors: Monitor[] | null;
|
||||
checks: Check[];
|
||||
checksCount: 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 navigate = useNavigate();
|
||||
|
||||
const getHeaders = (t: Function, uiTimezone: string) => {
|
||||
const headers: Header<Check>[] = [
|
||||
{
|
||||
id: "monitorName",
|
||||
content: t("common.table.headers.monitor"),
|
||||
render: (row) => {
|
||||
return (
|
||||
monitors?.find((monitor) => monitor.id === row.metadata.monitorId)?.name ||
|
||||
"N/A"
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: "Status",
|
||||
render: (row) => {
|
||||
return <StatusLabel status={row.status as MonitorStatus} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "date",
|
||||
content: t("common.table.headers.dateTime"),
|
||||
render: (row) => {
|
||||
return formatDateWithTz(
|
||||
row.createdAt,
|
||||
"ddd, MMMM D, YYYY, HH:mm A",
|
||||
uiTimezone
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "message",
|
||||
content: t("common.table.headers.message"),
|
||||
render: (row) => {
|
||||
return row.message || "N/A";
|
||||
},
|
||||
},
|
||||
];
|
||||
return headers;
|
||||
};
|
||||
|
||||
const headers = getHeaders(t, uiTimezone);
|
||||
|
||||
const handlePageChange = (
|
||||
_e: React.MouseEvent<HTMLButtonElement> | null,
|
||||
newPage: number
|
||||
) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleRowsPerPageChange = (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => {
|
||||
const value = Number(e.target.value);
|
||||
setPage(0);
|
||||
setRowsPerPage(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Table
|
||||
headers={headers}
|
||||
data={checks}
|
||||
onRowClick={(row) => {
|
||||
navigate(`/checks/${row.id}`);
|
||||
}}
|
||||
emptyViewText={t("pages.checks.table.empty")}
|
||||
/>
|
||||
<Pagination
|
||||
component="div"
|
||||
count={checksCount}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={handlePageChange}
|
||||
onRowsPerPageChange={handleRowsPerPageChange}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,176 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import {
|
||||
BasePage,
|
||||
TotalChecksBox,
|
||||
UpChecksBox,
|
||||
DownChecksBox,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import { HeaderTimeRange } from "@/Components/v2/common";
|
||||
import { Select } from "@/Components/v2/inputs";
|
||||
import { ChecksTable } from "./Components/ChecksTable";
|
||||
|
||||
import { MenuItem } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import type { ChecksSummary, ChecksResponse } from "@/Types/Check";
|
||||
|
||||
const Checks = () => {
|
||||
const { t } = useTranslation();
|
||||
const { monitorId } = useParams<{ monitorId?: string }>();
|
||||
|
||||
const [selectedMonitor, setSelectedMonitor] = useState<string>(monitorId || "0");
|
||||
const [dateRange, setDateRange] = useState<string>("recent");
|
||||
const [statusFilter, setStatusFilter] = useState<string>("down");
|
||||
const [page, setPage] = useState<number>(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
|
||||
|
||||
const monitorsUrl = "/monitors/team";
|
||||
const summaryUrl = `/checks/team/summary?dateRange=${dateRange}`;
|
||||
|
||||
const { data: monitorsResponse, isLoading: isLoadingMonitors } =
|
||||
useGet<Monitor[]>(monitorsUrl);
|
||||
|
||||
const { data: summaryResponse, isLoading: isLoadingSummary } =
|
||||
useGet<ChecksSummary>(summaryUrl);
|
||||
|
||||
const selectedMonitorType = monitorsResponse?.find(
|
||||
(m) => m.id === selectedMonitor
|
||||
)?.type;
|
||||
|
||||
const teamChecksUrl = useMemo(() => {
|
||||
if (selectedMonitor !== "0") return null;
|
||||
const params = new URLSearchParams();
|
||||
params.append("sortOrder", "desc");
|
||||
if (dateRange) params.append("dateRange", dateRange);
|
||||
if (statusFilter) params.append("filter", statusFilter);
|
||||
params.append("page", String(page));
|
||||
params.append("rowsPerPage", String(rowsPerPage));
|
||||
return `/checks/team?${params.toString()}`;
|
||||
}, [selectedMonitor, dateRange, statusFilter, page, rowsPerPage]);
|
||||
|
||||
const monitorChecksUrl = useMemo(() => {
|
||||
if (selectedMonitor === "0" || !selectedMonitorType) return null;
|
||||
const params = new URLSearchParams();
|
||||
params.append("type", selectedMonitorType);
|
||||
params.append("sortOrder", "desc");
|
||||
if (statusFilter) params.append("filter", statusFilter);
|
||||
if (dateRange) params.append("dateRange", dateRange);
|
||||
params.append("page", String(page));
|
||||
params.append("rowsPerPage", String(rowsPerPage));
|
||||
return `/checks/${selectedMonitor}?${params.toString()}`;
|
||||
}, [selectedMonitor, selectedMonitorType, dateRange, statusFilter, page, rowsPerPage]);
|
||||
|
||||
const {
|
||||
data: teamChecksData,
|
||||
isLoading: isLoadingTeamChecks,
|
||||
isValidating: isValidatingTeamChecks,
|
||||
} = useGet<ChecksResponse>(
|
||||
teamChecksUrl,
|
||||
{},
|
||||
{ keepPreviousData: true, refreshInterval: 30000 }
|
||||
);
|
||||
const {
|
||||
data: monitorChecksData,
|
||||
isLoading: isLoadingMonitorChecks,
|
||||
isValidating: isValidatingMonitorChecks,
|
||||
} = useGet<ChecksResponse>(
|
||||
monitorChecksUrl,
|
||||
{},
|
||||
{ keepPreviousData: true, refreshInterval: 30000 }
|
||||
);
|
||||
|
||||
const checks =
|
||||
selectedMonitor === "0"
|
||||
? (teamChecksData?.checks ?? [])
|
||||
: (monitorChecksData?.checks ?? []);
|
||||
const checksCount =
|
||||
selectedMonitor === "0"
|
||||
? (teamChecksData?.checksCount ?? 0)
|
||||
: (monitorChecksData?.checksCount ?? 0);
|
||||
|
||||
const isLoadingChecks =
|
||||
isLoadingTeamChecks ||
|
||||
isLoadingMonitorChecks ||
|
||||
isValidatingTeamChecks ||
|
||||
isValidatingMonitorChecks;
|
||||
|
||||
const isLoading = isLoadingMonitors || isLoadingSummary || isLoadingChecks;
|
||||
const totalChecks = summaryResponse?.totalChecks || 0;
|
||||
const downChecks = summaryResponse?.downChecks || 0;
|
||||
const upChecks = totalChecks - (summaryResponse?.downChecks || 0);
|
||||
|
||||
return (
|
||||
<BasePage>
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={4}
|
||||
>
|
||||
<TotalChecksBox n={totalChecks} />
|
||||
<UpChecksBox n={upChecks} />
|
||||
<DownChecksBox n={downChecks || 0} />
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
justifyContent="space-between"
|
||||
alignItems={{ xs: "stretch", md: "center" }}
|
||||
gap={2}
|
||||
>
|
||||
<Stack
|
||||
gap={2}
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
>
|
||||
<Select
|
||||
fullWidth
|
||||
value={selectedMonitor}
|
||||
onChange={(e: any) => {
|
||||
setSelectedMonitor(e.target.value);
|
||||
setPage(0);
|
||||
}}
|
||||
>
|
||||
<MenuItem value="0">{t("pages.checks.selects.monitor.all")}</MenuItem>
|
||||
{monitorsResponse?.map((monitor) => (
|
||||
<MenuItem
|
||||
key={monitor.id}
|
||||
value={monitor.id}
|
||||
>
|
||||
{monitor.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onChange={(e: any) => {
|
||||
setStatusFilter(e.target.value);
|
||||
setPage(0);
|
||||
}}
|
||||
>
|
||||
<MenuItem value="all">{t("pages.checks.selects.status.all")}</MenuItem>
|
||||
<MenuItem value="up">{t("pages.checks.selects.status.up")}</MenuItem>
|
||||
<MenuItem value="down">{t("pages.checks.selects.status.down")}</MenuItem>
|
||||
</Select>
|
||||
</Stack>
|
||||
<HeaderTimeRange
|
||||
isLoading={isLoading || isLoadingChecks}
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<ChecksTable
|
||||
monitors={monitorsResponse ?? null}
|
||||
checks={checks}
|
||||
checksCount={checksCount}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
setRowsPerPage={setRowsPerPage}
|
||||
/>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checks;
|
||||
@@ -33,7 +33,7 @@ import InfrastructureDetails from "../Pages/Infrastructure/Details/index.jsx";
|
||||
import ServerUnreachable from "../Pages/ServerUnreachable.jsx";
|
||||
|
||||
// Checks
|
||||
import Checks from "../Pages/Checks/index.jsx";
|
||||
import Checks from "../Pages/Checks/index";
|
||||
|
||||
// Incidents
|
||||
import Incidents from "../Pages/Incidents/index.jsx";
|
||||
@@ -163,7 +163,13 @@ const Routes = () => {
|
||||
/>
|
||||
<Route
|
||||
path="checks/:monitorId?"
|
||||
element={<Checks />}
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<Checks />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="incidents/:monitorId?"
|
||||
|
||||
@@ -222,9 +222,7 @@ export interface UptimeChecksResult {
|
||||
|
||||
export interface ChecksSummary {
|
||||
totalChecks: number;
|
||||
resolvedChecks: number;
|
||||
downChecks: number;
|
||||
cannotResolveChecks: number;
|
||||
}
|
||||
|
||||
export type CheckSnapshot = Omit<
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"monitor": "Monitor",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"type": "Type"
|
||||
"type": "Type",
|
||||
"dateTime": "Date & time"
|
||||
},
|
||||
"empty": "Nothing here"
|
||||
},
|
||||
@@ -170,6 +171,22 @@
|
||||
},
|
||||
"add": "Add",
|
||||
"monitors": "monitors",
|
||||
"components": {
|
||||
"headerTimeRange": {
|
||||
"labels": {
|
||||
"recent": "Recent",
|
||||
"day": "Day",
|
||||
"week": "Week",
|
||||
"month": "Month"
|
||||
},
|
||||
"description": {
|
||||
"recent": "Showing statistics for past 2 hours.",
|
||||
"day": "Showing statistics for past 24 hours.",
|
||||
"week": "Showing statistics for past 7 days.",
|
||||
"month": "Showing statistics for past 30 days."
|
||||
}
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"common": {
|
||||
"monitors": {
|
||||
@@ -177,6 +194,7 @@
|
||||
"up": "up",
|
||||
"down": "down",
|
||||
"paused": "paused",
|
||||
"total": "total",
|
||||
"initializing": "initializing"
|
||||
},
|
||||
"actions": {
|
||||
@@ -223,6 +241,17 @@
|
||||
"headers": {
|
||||
"dateTime": "Date & time",
|
||||
"statusCode": "Status code"
|
||||
},
|
||||
"empty": "No down checks in this time range"
|
||||
},
|
||||
"selects": {
|
||||
"monitor": {
|
||||
"all": "All monitors"
|
||||
},
|
||||
"status": {
|
||||
"all": "All",
|
||||
"down": "Down",
|
||||
"up": "Up"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ import mongoose from "mongoose";
|
||||
const SERVICE_NAME = "StatusService";
|
||||
|
||||
const dateRangeLookup: Record<string, Date | undefined> = {
|
||||
recent: new Date(new Date().setDate(new Date().getDate() - 2)),
|
||||
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
|
||||
hour: new Date(new Date().setHours(new Date().getHours() - 1)),
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
@@ -193,6 +193,13 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
};
|
||||
};
|
||||
|
||||
private mapDocuments = (documents: CheckDocument[]): Check[] => {
|
||||
if (!documents?.length) {
|
||||
return [];
|
||||
}
|
||||
return documents.map((doc) => this.toEntity(doc));
|
||||
};
|
||||
|
||||
createChecks = async (checks: Check[]) => {
|
||||
return await CheckModel.insertMany(checks);
|
||||
};
|
||||
@@ -221,9 +228,14 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
switch (filter) {
|
||||
case "all":
|
||||
break;
|
||||
case "up":
|
||||
matchStage.status = true;
|
||||
break;
|
||||
case "down":
|
||||
matchStage.status = false;
|
||||
break;
|
||||
case "resolve":
|
||||
matchStage.status = false;
|
||||
matchStage.statusCode = 5000;
|
||||
break;
|
||||
default:
|
||||
@@ -245,33 +257,17 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
|
||||
const checks = await CheckModel.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { createdAt: convertedSortOrder } },
|
||||
{
|
||||
$facet: {
|
||||
summary: [{ $count: "checksCount" }],
|
||||
checks: [{ $skip: skip }, { $limit: rowsPerPage }],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
checksCount: {
|
||||
$ifNull: [{ $arrayElemAt: ["$summary.checksCount", 0] }, 0],
|
||||
},
|
||||
checks: {
|
||||
$ifNull: ["$checks", []],
|
||||
},
|
||||
},
|
||||
},
|
||||
const [checksCount, checks] = await Promise.all([
|
||||
CheckModel.countDocuments(matchStage),
|
||||
CheckModel.find(matchStage).sort({ createdAt: convertedSortOrder }).skip(skip).limit(rowsPerPage).lean() as Promise<CheckDocument[]>,
|
||||
]);
|
||||
return checks[0];
|
||||
|
||||
return { checksCount, checks: this.mapDocuments(checks) };
|
||||
};
|
||||
|
||||
findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => {
|
||||
const matchStage: Record<string, any> = {
|
||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||
status: false,
|
||||
...(dateRangeLookup[dateRange] && {
|
||||
createdAt: {
|
||||
$gte: dateRangeLookup[dateRange],
|
||||
@@ -283,9 +279,14 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
switch (filter) {
|
||||
case "all":
|
||||
break;
|
||||
case "up":
|
||||
matchStage.status = true;
|
||||
break;
|
||||
case "down":
|
||||
matchStage.status = false;
|
||||
break;
|
||||
case "resolve":
|
||||
matchStage.status = false;
|
||||
matchStage.statusCode = 5000;
|
||||
break;
|
||||
default:
|
||||
@@ -306,26 +307,12 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
|
||||
const aggregatePipeline: any = [
|
||||
{ $match: matchStage },
|
||||
const [checksCount, checks] = await Promise.all([
|
||||
CheckModel.countDocuments(matchStage),
|
||||
CheckModel.find(matchStage).sort({ createdAt: parsedSortOrder }).skip(skip).limit(rowsPerPage).lean() as Promise<CheckDocument[]>,
|
||||
]);
|
||||
|
||||
{ $sort: { createdAt: parsedSortOrder } },
|
||||
{
|
||||
$facet: {
|
||||
summary: [{ $count: "checksCount" }],
|
||||
checks: [{ $skip: skip }, { $limit: rowsPerPage }],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
checksCount: { $arrayElemAt: ["$summary.checksCount", 0] },
|
||||
checks: "$checks",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const checks = await CheckModel.aggregate(aggregatePipeline);
|
||||
return checks[0];
|
||||
return { checksCount, checks: this.mapDocuments(checks) };
|
||||
};
|
||||
|
||||
findLatestByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap> => {
|
||||
@@ -366,29 +353,24 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
};
|
||||
|
||||
findSummaryByTeamId = async (teamId: string, dateRange: string) => {
|
||||
const matchStage = {
|
||||
const baseMatch = {
|
||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||
status: false,
|
||||
...(dateRangeLookup[dateRange] && {
|
||||
createdAt: {
|
||||
$gte: dateRangeLookup[dateRange],
|
||||
},
|
||||
}),
|
||||
};
|
||||
const checks = await CheckModel.aggregate([
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalChecks: { $sum: 1 },
|
||||
resolvedChecks: { $sum: { $cond: [{ $eq: ["$ack", true] }, 1, 0] } },
|
||||
downChecks: { $sum: { $cond: [{ $eq: ["$ack", false] }, 1, 0] } },
|
||||
cannotResolveChecks: { $sum: { $cond: [{ $eq: ["$statusCode", 5000] }, 1, 0] } },
|
||||
},
|
||||
},
|
||||
{ $project: { _id: 0 } },
|
||||
|
||||
const [totalResult, downResult] = await Promise.all([
|
||||
CheckModel.countDocuments(baseMatch),
|
||||
CheckModel.countDocuments({ ...baseMatch, status: false }),
|
||||
]);
|
||||
return checks[0] ?? { totalChecks: 0, resolvedChecks: 0, downChecks: 0, cannotResolveChecks: 0 };
|
||||
|
||||
return {
|
||||
totalChecks: totalResult,
|
||||
downChecks: downResult,
|
||||
};
|
||||
};
|
||||
|
||||
deleteByMonitorId = async (monitorId: string): Promise<number> => {
|
||||
@@ -562,7 +544,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
const checks = await CheckModel.find(matchStage).sort({ createdAt: -1 }).limit(25).lean();
|
||||
return {
|
||||
monitorType: "pagespeed" as const,
|
||||
checks: checks.map((doc) => this.toEntity(doc)),
|
||||
checks: this.mapDocuments(checks),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -172,9 +172,7 @@ export interface UptimeChecksResult {
|
||||
|
||||
export interface ChecksSummary {
|
||||
totalChecks: number;
|
||||
resolvedChecks: number;
|
||||
downChecks: number;
|
||||
cannotResolveChecks: number;
|
||||
}
|
||||
|
||||
export interface HasResponseTime {
|
||||
|
||||
@@ -111,7 +111,7 @@ const getMonitorByIdQueryValidation = joi.object({
|
||||
status: joi.boolean(),
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
dateRange: joi.string().valid("hour", "day", "week", "month", "all"),
|
||||
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
|
||||
numToDisplay: joi.number(),
|
||||
normalize: joi.boolean(),
|
||||
});
|
||||
@@ -307,7 +307,7 @@ const getChecksQueryValidation = joi.object({
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
|
||||
filter: joi.string().valid("all", "down", "resolve"),
|
||||
filter: joi.string().valid("all", "up", "down", "resolve"),
|
||||
ack: joi.boolean(),
|
||||
page: joi.number(),
|
||||
rowsPerPage: joi.number(),
|
||||
@@ -317,8 +317,8 @@ const getChecksQueryValidation = joi.object({
|
||||
const getTeamChecksQueryValidation = joi.object({
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
dateRange: joi.string().valid("hour", "day", "week", "month", "all"),
|
||||
filter: joi.string().valid("all", "down", "resolve"),
|
||||
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
|
||||
filter: joi.string().valid("all", "up", "down", "resolve"),
|
||||
ack: joi.boolean(),
|
||||
page: joi.number(),
|
||||
rowsPerPage: joi.number(),
|
||||
|
||||
Reference in New Issue
Block a user