diff --git a/client/src/Pages/v1/Infrastructure/Create/index.jsx b/client/src/Pages/v1/Infrastructure/Create/index.jsx index d4fd859ec..82cf80c1b 100644 --- a/client/src/Pages/v1/Infrastructure/Create/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/index.jsx @@ -13,7 +13,7 @@ import CustomAlertsSection from "./Components/CustomAlertsSection.jsx"; // Utils import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js"; -import { useParams } from "react-router-dom"; +import { useParams, useSearchParams } from "react-router-dom"; import { useState, useEffect } from "react"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; @@ -29,11 +29,15 @@ import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx"; const CreateInfrastructureMonitor = () => { const { monitorId } = useParams(); + const [searchParams] = useSearchParams(); const isCreate = typeof monitorId === "undefined"; const theme = useTheme(); const { t } = useTranslation(); + // Get monitor type from query params (default to hardware for backwards compatibility) + const monitorType = searchParams.get("type") || "hardware"; + // State const [https, setHttps] = useState(false); const [updateTrigger, setUpdateTrigger] = useState(false); diff --git a/client/src/Pages/v1/Infrastructure/Monitors/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/index.jsx index 6c3a657ab..bef4330e9 100644 --- a/client/src/Pages/v1/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Monitors/index.jsx @@ -1,5 +1,6 @@ // Components -import { Stack } from "@mui/material"; +import { Stack, Tab } from "@mui/material"; +import { TabContext, TabPanel } from "@mui/lab"; import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx"; import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; @@ -8,6 +9,7 @@ import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; import Filter from "./Components/Filters/index.jsx"; import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent/index.jsx"; +import CustomTabList from "@/Components/v1/Tab/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { useEffect, useState } from "react"; @@ -17,7 +19,6 @@ import { useFetchMonitorsByTeamId } from "../../../../Hooks/v1/monitorHooks.js"; import { useDispatch, useSelector } from "react-redux"; import { setRowsPerPage } from "../../../../Features/UI/uiSlice.js"; // Constants -const TYPES = ["hardware"]; const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }]; const InfrastructureMonitors = () => { @@ -26,6 +27,7 @@ const InfrastructureMonitors = () => { const dispatch = useDispatch(); // Local state + const [tab, setTab] = useState("hardware"); const [page, setPage] = useState(0); const [updateTrigger, setUpdateTrigger] = useState(false); const [selectedStatus, setSelectedStatus] = useState(undefined); @@ -39,6 +41,14 @@ const InfrastructureMonitors = () => { const { t } = useTranslation(); // Handlers + const handleTabChange = (event, newTab) => { + setTab(newTab); + setPage(0); // Reset to first page when changing tabs + setSearch(undefined); // Clear search + setSelectedStatus(undefined); // Clear filters + setToFilterStatus(undefined); + }; + const handleActionMenuDelete = () => { setUpdateTrigger(!updateTrigger); }; @@ -70,9 +80,12 @@ const InfrastructureMonitors = () => { const field = toFilterStatus !== undefined ? "status" : undefined; + // Determine monitor types based on active tab + const types = tab === "hardware" ? ["hardware"] : ["docker"]; + const [monitors, summary, isLoading, networkError] = useFetchMonitorsByTeamId({ limit: 1, - types: TYPES, + types: types, page, field: field, filter: toFilterStatus ?? search, @@ -82,55 +95,113 @@ const InfrastructureMonitors = () => { return ( <> - - - - - - - - - + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/server/src/db/v1/models/Check.js b/server/src/db/v1/models/Check.js index 789cebaa5..98dcdcf50 100755 --- a/server/src/db/v1/models/Check.js +++ b/server/src/db/v1/models/Check.js @@ -54,6 +54,35 @@ const networkInterfaceSchema = mongoose.Schema({ fifo_out: { type: Number, default: 0 }, }); +const portSchema = mongoose.Schema({ + port: { type: String }, + protocol: { type: String }, +}); + +const healthSchema = mongoose.Schema({ + healthy: { type: Boolean, default: false }, + source: { type: String, default: "" }, + message: { type: String, default: "" }, +}); + +const containerSchema = mongoose.Schema({ + container_id: { type: String, default: "" }, + container_name: { type: String, default: "" }, + status: { type: String, default: "" }, + health: { + type: healthSchema, + default: () => ({}), + }, + running: { type: Boolean, default: false }, + base_image: { type: String, default: "" }, + exposed_ports: { + type: [portSchema], + default: () => [], + }, + started_at: { type: Number, default: 0 }, + finished_at: { type: Number, default: 0 }, +}); + const CheckSchema = new mongoose.Schema( { // Common fields @@ -148,6 +177,12 @@ const CheckSchema = new mongoose.Schema( default: () => [], }, + // Docker fields + docker: { + type: [containerSchema], + default: () => [], + }, + // PageSpeed fields accessibility: { type: Number, diff --git a/server/src/service/v1/infrastructure/networkService.js b/server/src/service/v1/infrastructure/networkService.js index d364f373e..9346737f0 100644 --- a/server/src/service/v1/infrastructure/networkService.js +++ b/server/src/service/v1/infrastructure/networkService.js @@ -286,7 +286,7 @@ class NetworkService { } } - async requestDocker(monitor) { + async requestLocalDocker(monitor) { try { if (!monitor.url) { throw new Error("Monitor URL is required"); @@ -330,7 +330,7 @@ class NetworkService { this.logger.warn({ message: `No container found for "${monitor.url}".`, service: this.SERVICE_NAME, - method: "requestDocker", + method: "requestLocalDocker", details: { url: monitor.url }, }); @@ -350,7 +350,7 @@ class NetworkService { this.logger.warn({ message: `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`, service: this.SERVICE_NAME, - method: "requestDocker", + method: "requestLocalDocker", details: { url: monitor.url }, }); dockerResponse.status = 404; @@ -375,6 +375,112 @@ class NetworkService { } return dockerResponse; + } catch (err) { + err.service = this.SERVICE_NAME; + err.method = "requestLocalDocker"; + throw err; + } + } + + async requestCaptureDocker(monitor) { + try { + if (!monitor.url) { + throw new Error("Monitor URL is required"); + } + + const dockerResponse = { + monitorId: monitor._id, + teamId: monitor.teamId, + type: monitor.type, + }; + + // Build Capture API URL + const captureUrl = new URL(monitor.url); + const apiUrl = `${captureUrl.protocol}//${captureUrl.host}/api/v1/metrics/docker`; + + // Make request to Capture API + const { response, responseTime, error } = await this.timeRequest(async () => { + return await this.got(apiUrl, { + headers: { + Authorization: `Bearer ${monitor.secret || ""}`, + }, + timeout: { request: 10000 }, + responseType: "json", + }); + }); + + dockerResponse.responseTime = responseTime; + + // Handle errors + if (error) { + const statusCode = error.response?.statusCode || this.NETWORK_ERROR; + dockerResponse.status = false; + dockerResponse.code = statusCode; + + if (statusCode === 401) { + dockerResponse.message = "Invalid API secret for Capture server"; + } else if (statusCode === 404) { + dockerResponse.message = "Docker monitoring not available (Capture version too old or endpoint not found)"; + } else if (statusCode === 500) { + dockerResponse.message = "Docker daemon unavailable on Capture server"; + } else { + dockerResponse.message = error.message || "Failed to fetch Docker metrics from Capture"; + } + + return dockerResponse; + } + + // Parse response + const payload = response.body || response; + const { data, capture, errors } = payload; + + // Check for errors array + if (errors && errors.length > 0) { + // Check if it's a critical error (docker.client) + const dockerClientError = errors.find((e) => e.metric && e.metric.includes("docker.client")); + if (dockerClientError) { + dockerResponse.status = false; + dockerResponse.code = 500; + dockerResponse.message = dockerClientError.err || "Docker daemon unavailable"; + return dockerResponse; + } + } + + // Check if we have container data + if (!data || !Array.isArray(data) || data.length === 0) { + dockerResponse.status = false; + dockerResponse.code = 404; + dockerResponse.message = "No containers found on Capture server"; + dockerResponse.payload = { data: [], capture, errors: errors || [] }; + return dockerResponse; + } + + // Successful response - store all containers + dockerResponse.status = true; + dockerResponse.code = 200; + dockerResponse.message = `Found ${data.length} container(s) on Capture server`; + dockerResponse.payload = { data, capture, errors: errors || [] }; + + return dockerResponse; + } catch (err) { + err.service = this.SERVICE_NAME; + err.method = "requestCaptureDocker"; + throw err; + } + } + + async requestDocker(monitor) { + try { + if (!monitor.url) { + throw new Error("Monitor URL is required"); + } + + // Check if URL is HTTP/HTTPS (Capture API) or container name/ID (local) + if (monitor.url.startsWith("http://") || monitor.url.startsWith("https://")) { + return await this.requestCaptureDocker(monitor); + } else { + return await this.requestLocalDocker(monitor); + } } catch (err) { err.service = this.SERVICE_NAME; err.method = "requestDocker"; diff --git a/server/src/service/v1/infrastructure/statusService.js b/server/src/service/v1/infrastructure/statusService.js index 248d1c910..851d5c210 100755 --- a/server/src/service/v1/infrastructure/statusService.js +++ b/server/src/service/v1/infrastructure/statusService.js @@ -275,6 +275,13 @@ class StatusService { check.capture = payload?.capture ?? {}; check.net = net ?? {}; } + + if (type === "docker") { + const { data, capture, errors } = payload ?? {}; + check.docker = data ?? []; + check.errors = errors ?? []; + check.capture = capture ?? {}; + } return check; };