diff --git a/client/src/Components/v2/design-elements/StatusBox.tsx b/client/src/Components/v2/design-elements/StatusBox.tsx index 23cef3b37..9e322cf73 100644 --- a/client/src/Components/v2/design-elements/StatusBox.tsx +++ b/client/src/Components/v2/design-elements/StatusBox.tsx @@ -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 ( { position: "relative", flex: 1, padding: theme.spacing(4), + ...sx, }} > { const theme = useTheme(); return ( - + { label={t("pages.common.monitors.status.total")} n={n} color={theme.palette.primary.light} + sx={{ + maxWidth: "300px", + }} /> ); }; @@ -117,6 +127,9 @@ export const DownChecksBox = ({ n }: { n: number }) => { label={t("pages.common.monitors.status.down")} n={n} color={theme.palette.error.light} + sx={{ + maxWidth: "300px", + }} /> ); }; diff --git a/client/src/Pages/Checks/Components/ChecksTable.tsx b/client/src/Pages/Checks/Components/ChecksTable.tsx new file mode 100644 index 000000000..269e74035 --- /dev/null +++ b/client/src/Pages/Checks/Components/ChecksTable.tsx @@ -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, + selectedMonitorId, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + monitors: Monitor[] | null; + selectedMonitorId: string; + 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(); + + // DATA FETCHING HERE + + const getHeaders = (t: Function, uiTimezone: string) => { + const headers: Header[] = [ + { + 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 ; + }, + }, + { + id: "date", + content: t("checks.table.headers.dateTime"), + render: (row) => { + return formatDateWithTz( + row.createdAt, + "ddd, MMMM D, YYYY, HH:mm A", + uiTimezone + ); + }, + }, + { + id: "statusCode", + content: t("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 ( + + ); + }, + }, + { + 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 | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + const value = Number(e.target.value); + setPage(0); + setRowsPerPage(value); + }; + + return ( + + { + navigate(`/checks/${row.id}`); + }} + emptyViewText={t("checks.table.empty")} + /> + + + ); +}; diff --git a/client/src/Pages/Checks/index.tsx b/client/src/Pages/Checks/index.tsx index f692044e2..eeb555668 100644 --- a/client/src/Pages/Checks/index.tsx +++ b/client/src/Pages/Checks/index.tsx @@ -1,7 +1,10 @@ import Stack from "@mui/material/Stack"; import { BasePage, TotalChecksBox, DownChecksBox } from "@/Components/v2/design-elements"; +import { HeaderTimeRange } from "@/Components/v2/common"; +import { Select } from "@/Components/v2/inputs"; +import IncidentTable from "./Components/IncidentTable/index.jsx"; -import { useTheme } from "@mui/material"; +import { MenuItem, useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useState, useEffect, useMemo } from "react"; import { useParams } from "react-router-dom"; @@ -9,6 +12,12 @@ import { useGet } from "@/Hooks/UseApi"; import type { Monitor } from "@/Types/Monitor"; import type { ChecksSummary } from "@/Types/Check"; +interface MonitorLookupEntry { + id: string; + name: string; + type: string; +} + const Checks = () => { const { t } = useTranslation(); const theme = useTheme(); @@ -16,8 +25,9 @@ const Checks = () => { // Local state const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [filter, setFilter] = useState(undefined); + const [filter, setFilter] = useState(undefined); const [dateRange, setDateRange] = useState("hour"); + const [updateTrigger, setUpdateTrigger] = useState(false); // Data fetching with SWR const monitorsUrl = "/monitors/team"; @@ -35,8 +45,6 @@ const Checks = () => { error: summaryError, } = useGet(summaryUrl); - console.log(monitorsResponse, summaryResponse); - return ( { + + + + + ); }; diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index e584cd661..708cf4ffe 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -366,27 +366,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 }, - downChecks: { $sum: { $cond: [{ $eq: ["$ack", false] }, 1, 0] } }, - }, - }, - { $project: { _id: 0 } }, + + const [totalResult, downResult] = await Promise.all([ + CheckModel.countDocuments(baseMatch), + CheckModel.countDocuments({ ...baseMatch, status: false }), ]); - return checks[0] ?? { totalChecks: 0, downChecks: 0 }; + + return { + totalChecks: totalResult, + downChecks: downResult, + }; }; deleteByMonitorId = async (monitorId: string): Promise => {