diff --git a/client/src/components/layouts/sidebar/Menu.tsx b/client/src/components/layouts/sidebar/Menu.tsx index 71e81b226..cbf1e79e6 100644 --- a/client/src/components/layouts/sidebar/Menu.tsx +++ b/client/src/components/layouts/sidebar/Menu.tsx @@ -3,6 +3,7 @@ import { Monitor, Gauge, Server, + Boxes, AlertTriangle, LayoutList, ListChecks, @@ -36,6 +37,11 @@ export const getMenu = (t: Function, user: IUser) => { path: "infrastructure", icon: , }, + { + name: t("menu.docker"), + path: "docker", + icon: , + }, { name: t("menu.notifications"), path: "notification-channels", diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 65cd8b76f..cf7e86cfb 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -184,6 +184,7 @@ "incidents": "Incidents", "limitReached": "Your plan's limit has been reached", "no": "No", + "home": "Return home", "pause": "Pause", "range": { "all": "All", @@ -447,6 +448,7 @@ "checks": "Checks", "diagnostics": "Diagnostics", "discussions": "Discussions", + "docker": "Docker", "docs": "Documentation", "incidents": "Incidents", "infrastructure": "Infrastructure", @@ -513,6 +515,18 @@ "up": "up" } }, + "docker": { + "fallback": { + "actionButton": "Create a monitor!", + "checks": [ + "Discover running Docker containers and track their lifecycle", + "Determine container health based on runtime state or health checks", + "Track exposed ports and network availability", + "Provide visibility into what is running on each Docker host" + ], + "title": "A Docker monitor is used to:" + } + }, "infrastructure": { "fallback": { "actionButton": "Create a monitor!", diff --git a/client/src/pages/docker/DockerMonitors.tsx b/client/src/pages/docker/DockerMonitors.tsx new file mode 100644 index 000000000..b8156097e --- /dev/null +++ b/client/src/pages/docker/DockerMonitors.tsx @@ -0,0 +1,128 @@ +import { + MonitorBasePageWithStates, + UpStatusBox, + DownStatusBox, + PausedStatusBox, + InfoBox, +} from "@/components/design-elements"; +import { HeaderCreate } from "@/components/common"; +import Stack from "@mui/material/Stack"; +import { Dialog } from "@/components/inputs"; +import { DockerMonitorsTable } from "@/pages/docker/DockerMonitorsTable"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/types/api"; +import type { IMonitor, IMonitorWithStats } from "@/types/monitor"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useState } from "react"; +import { useDelete } from "@/hooks/UseApi"; +import { InitializingStatusBox } from "@/components/design-elements/StatusBox"; +import { config } from "@/config/index"; +import { useLimitReached } from "@/hooks/UsePlanEntitlements"; + +const GLOBAL_REFRESH = config.GLOBAL_REFRESH; + +const DockerMonitorsPage = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const [selectedMonitor, setSelectedMonitor] = useState(null); + const isDialogOpen = Boolean(selectedMonitor); + const [sortField] = useState("name"); + const [sortOrder] = useState<"asc" | "desc">("desc"); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const { deleteFn, loading: isDeleting } = useDelete(); + const requestParams = [ + "embedChecks=true", + "type=docker", + `sortField=${sortField}`, + `sortOrder=${sortOrder}`, + `page=${page}`, + `rowsPerPage=${rowsPerPage}`, + ]; + + const monitorRequestUrl = `/monitors?${requestParams.join("&")}`; + + const { response, loading, isValidating, error, refetch } = useGet< + ApiResponse + >( + monitorRequestUrl, + {}, + { + refreshInterval: GLOBAL_REFRESH, + keepPreviousData: true, + dedupingInterval: 0, + }, + { + useTeamIdAsKey: true, + } + ); + const monitors: IMonitor[] = response?.data?.monitors ?? ([] as IMonitor[]); + const count = response?.data?.count || 0; + const upCount = response?.data?.upCount || 0; + const downCount = response?.data?.downCount || 0; + const pausedCount = response?.data?.pausedCount || 0; + + const monitorLimitReached = useLimitReached("monitorsMax", count); + + const handleConfirm = async () => { + await deleteFn(`/monitors/${selectedMonitor?._id}`); + setSelectedMonitor(null); + refetch(); + }; + + const handleCancel = () => { + setSelectedMonitor(null); + }; + + return ( + + + + + + + + + + + + + ); +}; + +export default DockerMonitorsPage; diff --git a/client/src/pages/docker/DockerMonitorsTable.tsx b/client/src/pages/docker/DockerMonitorsTable.tsx new file mode 100644 index 000000000..6bf580f32 --- /dev/null +++ b/client/src/pages/docker/DockerMonitorsTable.tsx @@ -0,0 +1,68 @@ +import Typography from "@mui/material/Typography"; +import { Table, Pagination, StatusLabel } from "@/components/design-elements"; +import type { Header } from "@/components/design-elements/Table"; +import type { IMonitor } from "@/types/monitor"; +import { useTranslation } from "react-i18next"; + +export const DockerMonitorsTable = ({ + monitors, + count, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + monitors: IMonitor[]; + count: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; +}) => { + const { t } = useTranslation(); + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => setPage(newPage); + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + setPage(0); + setRowsPerPage(Number(e.target.value)); + }; + + const headers: Header[] = [ + { + id: "name", + content: ( + {t("common.table.headers.name")} + ), + render: (row) => row.name, + }, + { + id: "status", + content: ( + + {t("common.table.headers.status")} + + ), + render: (row) => , + }, + ]; + + return ( + <> + + + + ); +}; diff --git a/client/src/pages/errors/NotFound.tsx b/client/src/pages/errors/NotFound.tsx index cc9ad286d..ebd3e9181 100644 --- a/client/src/pages/errors/NotFound.tsx +++ b/client/src/pages/errors/NotFound.tsx @@ -31,7 +31,7 @@ const NotFound = ({ title = DefaultValue.title, desc = DefaultValue.desc }) => { sx={{ mt: theme.spacing(10) }} onClick={() => navigate("/")} > - {t("notFoundButton")} + {t("common.buttons.home")} diff --git a/client/src/routes/Router.tsx b/client/src/routes/Router.tsx index 9037e1498..5a52804a9 100644 --- a/client/src/routes/Router.tsx +++ b/client/src/routes/Router.tsx @@ -31,6 +31,7 @@ import InfraMonitorsPage from "@/pages/infrastructure/InfraMonitors"; import InfraDetailsPage from "@/pages/infrastructure/InfraDetails"; import InfraConfigurePage from "@/pages/infrastructure/InfraConfig"; import InfraCreatePage from "@/pages/infrastructure/InfraCreate"; +import DockerMonitorsPage from "@/pages/docker/DockerMonitors"; import MaintenancePage from "@/pages/maintenance/Maintenance"; import MaintenanceConfigPage from "@/pages/maintenance/MaintenanceConfig"; @@ -137,6 +138,7 @@ const Router = () => { /> } /> } /> + } /> } /> } />