From 885acf6fe6eb0c5638427a699deb2c047cdd4327 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 25 Feb 2026 19:34:24 +0000 Subject: [PATCH] all continents in table --- client/src/Hooks/UseApi.ts | 2 +- .../Details/Components/GeoChecksTable.tsx | 48 +----- client/src/Pages/Uptime/Details/index.tsx | 9 +- client/src/Types/GeoCheck.ts | 18 ++ .../geo-checks/IGeoChecksRepository.ts | 9 +- .../geo-checks/MongoGeoChecksRepository.ts | 159 ++++++++---------- server/src/types/geoCheck.ts | 13 ++ 7 files changed, 124 insertions(+), 134 deletions(-) diff --git a/client/src/Hooks/UseApi.ts b/client/src/Hooks/UseApi.ts index e1faa2b29..2a5d514f6 100644 --- a/client/src/Hooks/UseApi.ts +++ b/client/src/Hooks/UseApi.ts @@ -17,7 +17,7 @@ const fetcher = async (url: string, config?: AxiosRequestConfig) => { }; export const useGet = ( - url: string | null, + url: string | null | undefined, axiosConfig?: AxiosRequestConfig, swrConfig?: SWRConfiguration ) => { diff --git a/client/src/Pages/Uptime/Details/Components/GeoChecksTable.tsx b/client/src/Pages/Uptime/Details/Components/GeoChecksTable.tsx index f125e03b5..31ad6a3dd 100644 --- a/client/src/Pages/Uptime/Details/Components/GeoChecksTable.tsx +++ b/client/src/Pages/Uptime/Details/Components/GeoChecksTable.tsx @@ -1,50 +1,15 @@ import { Table, Pagination, StatusLabel } from "@/Components/design-elements"; import Box from "@mui/material/Box"; import type { Header } from "@/Components/design-elements"; -import type { GeoCheck } from "@/Types/GeoCheck"; +import type { FlatGeoCheck } from "@/Types/GeoCheck"; import { useTranslation } from "react-i18next"; import { formatDateWithTz } from "@/Utils/TimeUtils"; import type { RootState } from "@/Types/state"; import { useSelector } from "react-redux"; import prettyMilliseconds from "pretty-ms"; -// Flatten geo check results - each location becomes a separate row -interface FlattenedGeoCheckResult { - id: string; - monitorId: string; - createdAt: string; - location: { - continent: string; - country: string; - city: string; - }; - status: boolean; - statusCode: number; - timings: { - total: number; - }; -} - -const flattenGeoChecks = (geoChecks: GeoCheck[]): FlattenedGeoCheckResult[] => { - const flattened: FlattenedGeoCheckResult[] = []; - for (const check of geoChecks) { - for (const result of check.results) { - flattened.push({ - id: `${check.id}-${result.location.continent}-${result.location.city}`, - monitorId: check.metadata.monitorId, - createdAt: check.createdAt, - location: result.location, - status: result.status, - statusCode: result.statusCode, - timings: result.timings, - }); - } - } - return flattened; -}; - const getHeaders = (t: Function, uiTimezone: string) => { - const headers: Header[] = [ + const headers: Header[] = [ { id: "status", content: t("common.table.headers.status"), @@ -71,7 +36,9 @@ const getHeaders = (t: Function, uiTimezone: string) => { id: "location", content: t("pages.checks.table.headers.location"), render: (row) => { - const { continent, country, city } = row.location; + const location = row.location; + if (!location) return "N/A"; + const { continent, country, city } = location; return `${continent} - ${country}, ${city}`; }, }, @@ -95,7 +62,7 @@ export const GeoChecksTable = ({ rowsPerPage, setRowsPerPage, }: { - geoChecks: GeoCheck[]; + geoChecks: FlatGeoCheck[]; count: number; page: number; setPage: (page: number) => void; @@ -105,7 +72,6 @@ export const GeoChecksTable = ({ const { t } = useTranslation(); const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const headers = getHeaders(t, uiTimezone); - const flattenedData = flattenGeoChecks(geoChecks); const handlePageChange = ( _e: React.MouseEvent | null, @@ -126,7 +92,7 @@ export const GeoChecksTable = ({ { geoRowsPerPage, ]); - const { data: geoChecksTableData } = useGet( + const { data: geoChecksTableData } = useGet( geoChecksTableUrl, {}, { keepPreviousData: true, revalidateOnFocus: false } @@ -163,7 +167,6 @@ const UptimeDetailsPage = () => { const geoChecksForTable = geoChecksTableData?.geoChecks ?? []; const geoChecksCount = geoChecksTableData?.geoChecksCount ?? 0; - // Extract unique locations from geo checks data const geoLocations = monitor?.geoCheckLocations; const checks = checksData?.checks ?? []; diff --git a/client/src/Types/GeoCheck.ts b/client/src/Types/GeoCheck.ts index af625ec5b..ec46eeac1 100644 --- a/client/src/Types/GeoCheck.ts +++ b/client/src/Types/GeoCheck.ts @@ -43,6 +43,19 @@ export interface GeoCheck { updatedAt: string; } +export interface FlatGeoCheck { + id: string; + monitorId: string; + teamId: string; + type: string; + location: GeoCheckLocation; + status: boolean; + statusCode: number; + timings: GeoCheckTimings; + createdAt: string; + updatedAt: string; +} + export interface GroupedGeoCheck { bucketDate: string; continent: GeoContinent; @@ -60,3 +73,8 @@ export interface GeoChecksResponse { geoChecks: GeoCheck[]; geoChecksCount: number; } + +export interface FlatGeoChecksResponse { + geoChecks: FlatGeoCheck[]; + geoChecksCount: number; +} diff --git a/server/src/repositories/geo-checks/IGeoChecksRepository.ts b/server/src/repositories/geo-checks/IGeoChecksRepository.ts index 3eedf5fa3..fe4beb796 100644 --- a/server/src/repositories/geo-checks/IGeoChecksRepository.ts +++ b/server/src/repositories/geo-checks/IGeoChecksRepository.ts @@ -1,11 +1,16 @@ import type { GeoCheck, GroupedGeoCheck } from "@/types/geoCheck.js"; -import type { GeoContinent } from "@/types/geoCheck.js"; +import type { GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js"; export interface GeoChecksQueryResult { geoChecksCount: number; geoChecks: GeoCheck[]; } +export interface FlatGeoChecksQueryResult { + geoChecksCount: number; + geoChecks: FlatGeoCheck[]; +} + export interface IGeoChecksRepository { createGeoChecks(geoChecks: Omit[]): Promise; findByMonitorId( @@ -15,7 +20,7 @@ export interface IGeoChecksRepository { page: number, rowsPerPage: number, continents?: GeoContinent[] - ): Promise; + ): Promise; findByMonitorIdAndDateRange(monitorId: string, startDate: Date, endDate: Date): Promise; findGroupedByMonitorIdAndDateRange( monitorId: string, diff --git a/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts b/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts index cae1be74f..89688afec 100644 --- a/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts +++ b/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts @@ -1,6 +1,6 @@ import { IGeoChecksRepository } from "./IGeoChecksRepository.js"; -import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent } from "@/types/geoCheck.js"; -import type { GeoChecksQueryResult } from "./IGeoChecksRepository.js"; +import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js"; +import type { GeoChecksQueryResult, FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js"; import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js"; import mongoose from "mongoose"; @@ -112,15 +112,8 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { page: number, rowsPerPage: number, continents?: GeoContinent[] - ): Promise => { + ): Promise => { try { - this.logger.debug({ - message: "findByMonitorId called", - service: SERVICE_NAME, - method: "findByMonitorId", - details: { monitorId, continents, continentsLength: continents?.length }, - }); - const matchStage: Record = { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), ...(dateRangeLookup[dateRange] && { @@ -138,12 +131,6 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { } if (continents && continents.length > 0) { - this.logger.debug({ - message: "Filtering by continents", - service: SERVICE_NAME, - method: "findByMonitorId", - details: { continents }, - }); const pipeline: any[] = [ { $match: matchStage }, { $unwind: "$results" }, @@ -152,80 +139,78 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { "results.location.continent": { $in: continents }, }, }, - { - $group: { - _id: "$_id", - doc: { $first: "$$ROOT" }, - results: { $push: "$results" }, - }, - }, - { - $replaceRoot: { - newRoot: { - $mergeObjects: ["$doc", { results: "$results" }], - }, - }, - }, - { $sort: { createdAt: convertedSortOrder } }, - { $skip: skip }, - { $limit: rowsPerPage }, ]; - - const [countPipeline, dataResults] = await Promise.all([ - GeoCheckModel.aggregate([ - { $match: matchStage }, - { $unwind: "$results" }, - { $match: { "results.location.continent": { $in: continents } } }, - { $group: { _id: "$_id" } }, - { $count: "count" }, - ]), - GeoCheckModel.aggregate(pipeline), - ]); - - const geoChecksCount = countPipeline[0]?.count || 0; - const geoChecks = dataResults.map(this.toEntity); - - return { geoChecksCount, geoChecks }; } else { - this.logger.debug({ - message: "No continent filtering - returning all checks", - service: SERVICE_NAME, - method: "findByMonitorId", - }); - - // Still need to unwind to show each location as a separate row - const pipeline: any[] = [ - { $match: matchStage }, - { $unwind: "$results" }, - { - $group: { - _id: "$_id", - doc: { $first: "$$ROOT" }, - results: { $push: "$results" }, - }, - }, - { - $replaceRoot: { - newRoot: { - $mergeObjects: ["$doc", { results: "$results" }], - }, - }, - }, - { $sort: { createdAt: convertedSortOrder } }, - { $skip: skip }, - { $limit: rowsPerPage }, - ]; - - const [countPipeline, dataResults] = await Promise.all([ - GeoCheckModel.aggregate([{ $match: matchStage }, { $unwind: "$results" }, { $group: { _id: "$_id" } }, { $count: "count" }]), - GeoCheckModel.aggregate(pipeline), - ]); - - const geoChecksCount = countPipeline[0]?.count || 0; - const geoChecks = dataResults.map(this.toEntity); - - return { geoChecksCount, geoChecks }; + const pipeline: any[] = [{ $match: matchStage }, { $unwind: "$results" }]; } + + // Common pipeline stages for both paths + const pipeline: any[] = [ + { $match: matchStage }, + { $unwind: "$results" }, + // Filter by continent if specified + ...(continents && continents.length > 0 + ? [ + { + $match: { + "results.location.continent": { $in: continents }, + }, + }, + ] + : []), + // Project to flat structure + { + $project: { + _id: 0, + monitorId: "$metadata.monitorId", + teamId: "$metadata.teamId", + type: "$metadata.type", + location: "$results.location", + status: "$results.status", + statusCode: "$results.statusCode", + timings: "$results.timings", + createdAt: 1, + updatedAt: 1, + }, + }, + { $sort: { createdAt: convertedSortOrder } }, + { $skip: skip }, + { $limit: rowsPerPage }, + ]; + + // Count pipeline + const countPipeline: any[] = [ + { $match: matchStage }, + { $unwind: "$results" }, + ...(continents && continents.length > 0 + ? [ + { + $match: { + "results.location.continent": { $in: continents }, + }, + }, + ] + : []), + { $count: "count" }, + ]; + + const [countResult, dataResults] = await Promise.all([GeoCheckModel.aggregate(countPipeline), GeoCheckModel.aggregate(pipeline)]); + + const geoChecksCount = countResult[0]?.count || 0; + const geoChecks: FlatGeoCheck[] = dataResults.map((doc) => ({ + id: `${doc.monitorId.toString()}-${new Date(doc.createdAt).getTime()}-${doc.location.continent}-${doc.location.city}-${Math.random().toString(36).substring(2, 15)}`, + monitorId: doc.monitorId.toString(), + teamId: doc.teamId.toString(), + type: doc.type, + location: doc.location, + status: doc.status, + statusCode: doc.statusCode, + timings: doc.timings, + createdAt: new Date(doc.createdAt).toISOString(), + updatedAt: new Date(doc.updatedAt).toISOString(), + })); + + return { geoChecksCount, geoChecks }; } catch (error: any) { this.logger.error({ message: `Error finding geo checks by monitor ID: ${error.message}`, diff --git a/server/src/types/geoCheck.ts b/server/src/types/geoCheck.ts index 742083a27..548c4c65f 100644 --- a/server/src/types/geoCheck.ts +++ b/server/src/types/geoCheck.ts @@ -45,6 +45,19 @@ export interface GeoCheck { updatedAt: string; } +export interface FlatGeoCheck { + id: string; + monitorId: string; + teamId: string; + type: string; + location: GeoCheckLocation; + status: boolean; + statusCode: number; + timings: GeoCheckTimings; + createdAt: string; + updatedAt: string; +} + export interface GroupedGeoCheck { bucketDate: string; continent: GeoContinent;