diff --git a/client/src/Components/monitors/HeaderGeoTabs.tsx b/client/src/Components/monitors/HeaderGeoTabs.tsx index 06753bc14..7351078d4 100644 --- a/client/src/Components/monitors/HeaderGeoTabs.tsx +++ b/client/src/Components/monitors/HeaderGeoTabs.tsx @@ -28,8 +28,6 @@ export const HeaderGeoTabs = ({ onLocationChange(newValue); }; - console.log(locations); - return ( { + 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"), render: (row) => { - const firstResult = row.results[0]; - const status = firstResult?.status ? "up" : "down"; + const status = row.status ? "up" : "down"; return ; }, }, @@ -30,17 +64,14 @@ const getHeaders = (t: Function, uiTimezone: string) => { id: "statusCode", content: t("pages.checks.table.headers.statusCode"), render: (row) => { - const firstResult = row.results[0]; - return firstResult?.statusCode || "N/A"; + return row.statusCode || "N/A"; }, }, { id: "location", content: t("pages.checks.table.headers.location"), render: (row) => { - const firstResult = row.results[0]; - if (!firstResult) return "N/A"; - const { continent, country, city } = firstResult.location; + const { continent, country, city } = row.location; return `${continent} - ${country}, ${city}`; }, }, @@ -48,9 +79,8 @@ const getHeaders = (t: Function, uiTimezone: string) => { id: "responseTime", content: t("common.table.headers.responseTime"), render: (row) => { - const firstResult = row.results[0]; - if (!firstResult?.timings?.total) return "N/A"; - return prettyMilliseconds(firstResult.timings.total, { compact: true }); + if (!row.timings?.total) return "N/A"; + return prettyMilliseconds(row.timings.total, { compact: true }); }, }, ]; @@ -75,6 +105,7 @@ 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, @@ -95,7 +126,7 @@ export const GeoChecksTable = ({ { params.append("dateRange", dateRange); params.append("page", String(geoPage)); params.append("rowsPerPage", String(geoRowsPerPage)); - if (selectedLocation) { - params.append("continent", selectedLocation); - } return `/geo-checks/${monitorId}?${params.toString()}`; }, [ monitorId, @@ -155,7 +152,6 @@ const UptimeDetailsPage = () => { dateRange, geoPage, geoRowsPerPage, - selectedLocation, ]); const { data: geoChecksTableData } = useGet( diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 662f35b50..a11e3fc65 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -142,14 +142,19 @@ class MonitorController { const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); const dateRange = requireString(req?.query?.dateRange, "dateRange"); - const continent = optionalString(req?.query?.continent, "continent") as GeoContinent | undefined; + const continentParam = req?.query?.continent; + const continents = continentParam + ? Array.isArray(continentParam) + ? (continentParam as GeoContinent[]) + : [continentParam as GeoContinent] + : undefined; const teamId = requireTeamId(req?.user?.teamId); const data = await this.monitorService.getGeoChecksByMonitorId({ teamId, monitorId, dateRange, - continent, + continents, }); return res.status(200).json({ diff --git a/server/src/repositories/geo-checks/IGeoChecksRepository.ts b/server/src/repositories/geo-checks/IGeoChecksRepository.ts index 516bbd7fc..3eedf5fa3 100644 --- a/server/src/repositories/geo-checks/IGeoChecksRepository.ts +++ b/server/src/repositories/geo-checks/IGeoChecksRepository.ts @@ -14,7 +14,7 @@ export interface IGeoChecksRepository { dateRange: string, page: number, rowsPerPage: number, - continent?: GeoContinent + continents?: GeoContinent[] ): Promise; findByMonitorIdAndDateRange(monitorId: string, startDate: Date, endDate: Date): Promise; findGroupedByMonitorIdAndDateRange( @@ -22,7 +22,7 @@ export interface IGeoChecksRepository { startDate: Date, endDate: Date, dateFormat: string, - continent?: GeoContinent + continents?: GeoContinent[] ): Promise; deleteByMonitorId(monitorId: string): Promise; deleteByTeamId(teamId: string): Promise; diff --git a/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts b/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts index e4a6e14e5..cae1be74f 100644 --- a/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts +++ b/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts @@ -111,9 +111,16 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { dateRange: string, page: number, rowsPerPage: number, - continent?: GeoContinent + continents?: GeoContinent[] ): 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] && { @@ -130,13 +137,19 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { skip = page * rowsPerPage; } - if (continent) { + 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" }, { $match: { - "results.location.continent": continent, + "results.location.continent": { $in: continents }, }, }, { @@ -162,7 +175,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { GeoCheckModel.aggregate([ { $match: matchStage }, { $unwind: "$results" }, - { $match: { "results.location.continent": continent } }, + { $match: { "results.location.continent": { $in: continents } } }, { $group: { _id: "$_id" } }, { $count: "count" }, ]), @@ -174,12 +187,44 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { return { geoChecksCount, geoChecks }; } else { - const [geoChecksCount, docs] = await Promise.all([ - GeoCheckModel.countDocuments(matchStage), - GeoCheckModel.find(matchStage).sort({ createdAt: convertedSortOrder }).skip(skip).limit(rowsPerPage).lean() as Promise, + 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), ]); - return { geoChecksCount, geoChecks: docs.map(this.toEntity) }; + const geoChecksCount = countPipeline[0]?.count || 0; + const geoChecks = dataResults.map(this.toEntity); + + return { geoChecksCount, geoChecks }; } } catch (error: any) { this.logger.error({ @@ -217,7 +262,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { startDate: Date, endDate: Date, dateFormat: string, - continent?: string + continents?: GeoContinent[] ): Promise => { try { const pipeline: any[] = [ @@ -236,11 +281,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { $unwind: "$results", }, // Filter by continent if specified - ...(continent + ...(continents && continents.length > 0 ? [ { $match: { - "results.location.continent": continent, + "results.location.continent": { $in: continents }, }, }, ] diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index e104b9ea4..ff0f0f31c 100644 --- a/server/src/service/business/geoChecksService.ts +++ b/server/src/service/business/geoChecksService.ts @@ -177,10 +177,19 @@ class GeoChecksService implements IGeoChecksService { } let { sortOrder, dateRange, page, rowsPerPage, continent } = query; + const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined; + + this.logger.debug({ + message: "getGeoChecksByMonitor query params", + service: SERVICE_NAME, + method: "getGeoChecksByMonitor", + details: { continent, continents, query }, + }); + const parsedPage = page ? parseInt(page) : page; const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage; - const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continent); + const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents); return result; }; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 7006d7bc8..662e5775a 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -38,7 +38,7 @@ export interface IMonitorService { getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; - getGeoChecksByMonitorId(args: { teamId: string; monitorId: string; dateRange: string; continent?: GeoContinent }): Promise; + getGeoChecksByMonitorId(args: { teamId: string; monitorId: string; dateRange: string; continents?: GeoContinent[] }): Promise; getMonitorById(args: { teamId: string; monitorId: string }): Promise; getMonitorsByTeamId(args: { teamId: string; @@ -327,12 +327,12 @@ export class MonitorService implements IMonitorService { teamId, monitorId, dateRange, - continent, + continents, }: { teamId: string; monitorId: string; dateRange: string; - continent?: GeoContinent; + continents?: GeoContinent[]; }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { @@ -350,7 +350,7 @@ export class MonitorService implements IMonitorService { start, end, this.getDateFormat(rangeKey), - continent + continents ); return { groupedGeoChecks }; diff --git a/server/src/validation/joi.ts b/server/src/validation/joi.ts index 26ba7b26a..4002c97e6 100755 --- a/server/src/validation/joi.ts +++ b/server/src/validation/joi.ts @@ -324,7 +324,7 @@ const getChecksQueryValidation = joi.object({ page: joi.number(), rowsPerPage: joi.number(), status: joi.boolean(), - continent: joi.string().valid(...GeoContinents), + continent: joi.alternatives().try(joi.string().valid(...GeoContinents), joi.array().items(joi.string().valid(...GeoContinents))), }); const getTeamChecksQueryValidation = joi.object({