diff --git a/client/src/components/monitors/CardDockerContainer.tsx b/client/src/components/monitors/CardDockerContainer.tsx new file mode 100644 index 000000000..fe670aea9 --- /dev/null +++ b/client/src/components/monitors/CardDockerContainer.tsx @@ -0,0 +1,104 @@ +import Stack from "@mui/material/Stack"; +import Grid from "@mui/material/Grid"; +import Typography from "@mui/material/Typography"; +import { useTranslation } from "react-i18next"; +import { BaseBox } from "@/components/design-elements"; + +type ExposedPort = { port: string; protocol: string }; + +export type DockerContainerSummary = { + container_id: string; + container_name: string; + base_image?: string; + exposed_ports?: ExposedPort[]; + status?: string; + health?: { healthy?: boolean; source?: string; message?: string }; +}; + +const shortId = (id?: string) => (id ? id.slice(0, 12) : "-"); +const portsText = (ports?: ExposedPort[]) => + (ports || []) + .map((p) => `${p.port}${p.protocol ? "/" + p.protocol : ""}`) + .join(", "); + +export const CardDockerContainer = ({ + container, +}: { + container: DockerContainerSummary; +}) => { + const { t } = useTranslation(); + return ( + + + + {t("monitors.docker.details.dockerContainerCard.title", { + name: container.container_name || "-", + })} + + + + + + {t("monitors.docker.details.dockerContainerCard.fields.status")} + + + + {container.status || "-"} + + + + + {t("monitors.docker.details.dockerContainerCard.fields.health")} + + + + + {container.health?.healthy === true + ? t( + "monitors.docker.details.dockerContainerCard.values.healthy" + ) + : container.health?.healthy === false + ? t( + "monitors.docker.details.dockerContainerCard.values.unhealthy" + ) + : "-"} + + + + + + {t("monitors.docker.details.dockerContainerCard.fields.id")} + + + + + {shortId(container.container_id)} + + + + + + {t("monitors.docker.details.dockerContainerCard.fields.image")} + + + + + {container.base_image || "-"} + + + + + + {t("monitors.docker.details.dockerContainerCard.fields.ports")} + + + + + {portsText(container.exposed_ports)} + + + + + + ); +}; diff --git a/client/src/components/monitors/HistogramDockerPercentage.tsx b/client/src/components/monitors/HistogramDockerPercentage.tsx new file mode 100644 index 000000000..1888e76a1 --- /dev/null +++ b/client/src/components/monitors/HistogramDockerPercentage.tsx @@ -0,0 +1,77 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { BaseChart } from "@/components/monitors/Chart"; +import { + ResponsiveContainer, + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, +} from "recharts"; +import { XTick } from "@/components/monitors/ChartResponseTime"; +import { useTheme } from "@mui/material/styles"; + +export const HistogramDockerPercentage = ({ + data, + dataKey, + range, + title, + emptyText, +}: { + data: any[]; + dataKey: string; + range: string; + title: string; + emptyText?: string; +}) => { + const theme = useTheme(); + const hasData = Array.isArray(data) && data.length > 0; + + return ( + + {!hasData ? ( + + {emptyText || "No data"} + + ) : ( + + + + + + + + + + } + /> + + + + + )} + + ); +}; diff --git a/client/src/components/monitors/index.tsx b/client/src/components/monitors/index.tsx index 6003f60e8..6b4f37ce9 100644 --- a/client/src/components/monitors/index.tsx +++ b/client/src/components/monitors/index.tsx @@ -5,5 +5,6 @@ export { HeaderControls } from "./HeaderControls"; export { ChartResponseTime } from "./ChartResponseTime"; export { HistogramPageSpeed } from "./HistogramPageSpeed"; export { HistogramInfrastructure } from "./HistogramInfrastructure"; - +export { CardDockerContainer } from "./CardDockerContainer"; export { HeaderFilter } from "./HeaderFilter"; +export { HistogramDockerPercentage } from "./HistogramDockerPercentage"; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 8f08d9a76..6c9879ca4 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -149,6 +149,7 @@ } } }, + "common": { "appName": "Checkmate", "breadcrumbs": { @@ -530,6 +531,30 @@ "headers": { "runningContainers": "Running containers" } + }, + "details": { + "dockerContainerCard": { + "title": "Container: {{name}}", + "fields": { + "status": "Status", + "health": "Health", + "id": "ID", + "image": "Image", + "ports": "Ports" + }, + "values": { + "healthy": "Healthy", + "unhealthy": "Unhealthy" + } + }, + "healthyChart": { + "title": "Healthy container %", + "empty": "No data" + }, + "runningChart": { + "title": "Running container %", + "empty": "No data" + } } }, "infrastructure": { diff --git a/client/src/pages/docker/DockerDetails.tsx b/client/src/pages/docker/DockerDetails.tsx index c6f14ae56..6e22eda1b 100644 --- a/client/src/pages/docker/DockerDetails.tsx +++ b/client/src/pages/docker/DockerDetails.tsx @@ -1,7 +1,11 @@ import { BasePage, StatBox } from "@/components/design-elements"; -import { HeaderControls, HeaderRange } from "@/components/monitors"; +import { + HeaderControls, + HeaderRange, + HistogramDockerPercentage, +} from "@/components/monitors"; import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; +import { CardDockerContainer } from "@/components/monitors"; import { useTranslation } from "react-i18next"; import prettyMilliseconds from "pretty-ms"; @@ -35,6 +39,8 @@ const DockerDetailsPage = () => { const stats = response?.data?.stats; const checks = (response?.data?.checks || []) as any; const latestCheck = checks[0]; + const snapshot = latestCheck?.runningContainersSnapshot || []; + console.log(checks); const palette = getStatusPalette(monitor?.status || "initializing"); const streakDuration = stats?.currentStreakStartedAt @@ -76,11 +82,25 @@ const DockerDetailsPage = () => { /> - - - {JSON.stringify(latestCheck, null, 2)} - + + {snapshot.map((c: any) => ( + + ))} + + ); };