diff --git a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx index 57b61473a..9b8b9dcec 100644 --- a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx +++ b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx @@ -2,6 +2,8 @@ import Stack from "@mui/material/Stack"; 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 Box from "@mui/material/Box"; interface MonitorTimeFrameHeaderProps { isLoading?: boolean; @@ -72,6 +74,9 @@ export const HeaderTimeRange = ({ alignItems="center" gap={theme.spacing(4)} > + + {isLoading && } + Showing statistics for past{" "} {dateRange === "recent" diff --git a/client/src/Components/v2/design-elements/StatusBox.tsx b/client/src/Components/v2/design-elements/StatusBox.tsx index 9e322cf73..d48871a2a 100644 --- a/client/src/Components/v2/design-elements/StatusBox.tsx +++ b/client/src/Components/v2/design-elements/StatusBox.tsx @@ -133,6 +133,20 @@ export const DownChecksBox = ({ n }: { n: number }) => { /> ); }; +export const UpChecksBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; export const InitializingStatusBox = ({ n }: { n: number }) => { const theme = useTheme(); diff --git a/client/src/Pages/Checks/Components/ChecksTable.tsx b/client/src/Pages/Checks/Components/ChecksTable.tsx index 9088c2cef..e48af37b8 100644 --- a/client/src/Pages/Checks/Components/ChecksTable.tsx +++ b/client/src/Pages/Checks/Components/ChecksTable.tsx @@ -20,6 +20,7 @@ import { useGet } from "@/Hooks/UseApi"; export const ChecksTable = ({ monitors, selectedMonitorId, + statusFilter, dateRange, page, setPage, @@ -28,6 +29,7 @@ export const ChecksTable = ({ }: { monitors: Monitor[] | null; selectedMonitorId: string; + statusFilter: string; dateRange: string; page: number; setPage: (page: number) => void; @@ -38,40 +40,36 @@ export const ChecksTable = ({ const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const navigate = useNavigate(); - // Get selected monitor type const selectedMonitorType = monitors?.find((m) => m.id === selectedMonitorId)?.type; - // Team checks URL (when selectedMonitorId === "0") const teamChecksUrl = useMemo(() => { if (selectedMonitorId !== "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()}`; - }, [selectedMonitorId, dateRange, page, rowsPerPage]); + }, [selectedMonitorId, dateRange, statusFilter, page, rowsPerPage]); - // Monitor checks URL (when specific monitor selected) const monitorChecksUrl = useMemo(() => { if (selectedMonitorId === "0" || !selectedMonitorType) return null; const params = new URLSearchParams(); params.append("type", selectedMonitorType); params.append("sortOrder", "desc"); - params.append("status", "false"); + if (statusFilter) params.append("filter", statusFilter); if (dateRange) params.append("dateRange", dateRange); params.append("page", String(page)); params.append("rowsPerPage", String(rowsPerPage)); return `/checks/${selectedMonitorId}?${params.toString()}`; - }, [selectedMonitorId, selectedMonitorType, dateRange, page, rowsPerPage]); + }, [selectedMonitorId, selectedMonitorType, dateRange, statusFilter, page, rowsPerPage]); - // Fetch data - null key skips the request const { data: teamData, isLoading: isLoadingTeam } = useGet(teamChecksUrl); const { data: monitorData, isLoading: isLoadingMonitor } = useGet(monitorChecksUrl); - // Select correct data based on selection const checks = selectedMonitorId === "0" ? (teamData?.checks ?? []) : (monitorData?.checks ?? []); const checksCount = @@ -101,7 +99,7 @@ export const ChecksTable = ({ }, { id: "date", - content: t("checks.table.headers.dateTime"), + content: t("common.table.headers.dateTime"), render: (row) => { return formatDateWithTz( row.createdAt, @@ -112,7 +110,7 @@ export const ChecksTable = ({ }, { id: "statusCode", - content: t("checks.table.headers.statusCode"), + content: t("pages.checks.table.headers.statusCode"), render: (row) => { const code = row.statusCode; if (!code) return "N/A"; @@ -161,7 +159,7 @@ export const ChecksTable = ({ onRowClick={(row) => { navigate(`/checks/${row.id}`); }} - emptyViewText={t("checks.table.empty")} + emptyViewText={t("pages.checks.table.empty")} /> { const { t } = useTranslation(); - const theme = useTheme(); const { monitorId } = useParams<{ monitorId?: string }>(); // Local state const [selectedMonitor, setSelectedMonitor] = useState(monitorId || "0"); const [dateRange, setDateRange] = useState("recent"); + const [statusFilter, setStatusFilter] = useState("down"); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); @@ -27,17 +32,16 @@ const Checks = () => { const monitorsUrl = "/monitors/team"; const summaryUrl = `/checks/team/summary?dateRange=${dateRange}`; - const { - data: monitorsResponse, - isLoading: isLoadingMonitors, - error: monitorsError, - } = useGet(monitorsUrl); + const { data: monitorsResponse, isLoading: isLoadingMonitors } = + useGet(monitorsUrl); - const { - data: summaryResponse, - isLoading: isLoadingSummary, - error: summaryError, - } = useGet(summaryUrl); + const { data: summaryResponse, isLoading: isLoadingSummary } = + useGet(summaryUrl); + + const isLoading = isLoadingMonitors || isLoadingSummary; + const totalChecks = summaryResponse?.totalChecks || 0; + const downChecks = summaryResponse?.downChecks || 0; + const upChecks = totalChecks - (summaryResponse?.downChecks || 0); return ( @@ -45,32 +49,52 @@ const Checks = () => { direction={{ xs: "column", md: "row" }} gap={4} > - - + + + - + + + @@ -79,6 +103,7 @@ const Checks = () => { { const matchStage: Record = { "metadata.teamId": new mongoose.Types.ObjectId(teamId), - status: false, ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], @@ -283,9 +287,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: diff --git a/server/src/validation/joi.ts b/server/src/validation/joi.ts index 8871c29e2..1d5fcbbf0 100755 --- a/server/src/validation/joi.ts +++ b/server/src/validation/joi.ts @@ -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(),