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) => (
+
+ ))}
+
+
);
};