diff --git a/client/src/Components/design-elements/BaseChart.tsx b/client/src/Components/design-elements/BaseChart.tsx
index 319a4ad51..e5233eb0f 100644
--- a/client/src/Components/design-elements/BaseChart.tsx
+++ b/client/src/Components/design-elements/BaseChart.tsx
@@ -10,6 +10,7 @@ import { useTheme } from "@mui/material/styles";
type BaseChartProps = React.PropsWithChildren<{
icon?: React.ReactNode;
title: string;
+ rightTitle?: string;
width?: number | string;
maxWidth?: number | string;
flexBasis?: number | string;
@@ -20,6 +21,7 @@ type BaseChartProps = React.PropsWithChildren<{
export const BaseChart = ({
children,
title,
+ rightTitle,
width = "100%",
maxWidth = "100%",
flexBasis = "0%",
@@ -47,13 +49,27 @@ export const BaseChart = ({
gap={theme.spacing(LAYOUT.MD)}
flex={1}
>
-
- {title}
-
+
+ {title}
+
+ {rightTitle && (
+
+ {rightTitle}
+
+ )}
+
{children}
diff --git a/client/src/Components/monitors/charts/ChartUtils.ts b/client/src/Components/monitors/charts/ChartUtils.ts
deleted file mode 100644
index 0516eaac3..000000000
--- a/client/src/Components/monitors/charts/ChartUtils.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-export const computeYAxisCap = (values: number[]): number | undefined => {
- const sorted = values.slice().sort((a, b) => a - b);
- if (sorted.length < 2) return undefined;
- const p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))];
- return p95 > 0 ? Math.ceil(p95 * 2) : undefined;
-};
-
-export const PHASE_KEYS = [
- "avgDns",
- "avgTcp",
- "avgTls",
- "avgRequest",
- "avgFirstByte",
- "avgDownload",
-] as const;
-
-export const PHASE_COLOR_KEYS = {
- avgDns: "dns",
- avgTcp: "tcp",
- avgTls: "tls",
- avgRequest: "request",
- avgFirstByte: "firstByte",
- avgDownload: "download",
-} as const;
-
-// i18n keys, resolved with t() at render
-export const PHASE_LABEL_KEYS: Record<(typeof PHASE_KEYS)[number], string> = {
- avgDns: "common.charts.phases.dns",
- avgTcp: "common.charts.phases.tcp",
- avgTls: "common.charts.phases.tls",
- avgRequest: "common.charts.phases.request",
- avgFirstByte: "common.charts.phases.firstByte",
- avgDownload: "common.charts.phases.download",
-};
diff --git a/client/src/Components/monitors/charts/ChartUtils.tsx b/client/src/Components/monitors/charts/ChartUtils.tsx
new file mode 100644
index 000000000..c90fdb6d5
--- /dev/null
+++ b/client/src/Components/monitors/charts/ChartUtils.tsx
@@ -0,0 +1,113 @@
+export const computeYAxisCap = (values: number[]): number | undefined => {
+ const sorted = values.slice().sort((a, b) => a - b);
+ if (sorted.length < 2) return undefined;
+ const p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))];
+ return p95 > 0 ? Math.ceil(p95 * 2) : undefined;
+};
+
+export const PHASE_KEYS = [
+ "avgDns",
+ "avgTcp",
+ "avgTls",
+ "avgRequest",
+ "avgFirstByte",
+ "avgDownload",
+] as const;
+
+export const PHASE_COLOR_KEYS = {
+ avgDns: "dns",
+ avgTcp: "tcp",
+ avgTls: "tls",
+ avgRequest: "request",
+ avgFirstByte: "firstByte",
+ avgDownload: "download",
+} as const;
+
+// i18n keys, resolved with t() at render
+export const PHASE_LABEL_KEYS: Record<(typeof PHASE_KEYS)[number], string> = {
+ avgDns: "common.charts.phases.dns",
+ avgTcp: "common.charts.phases.tcp",
+ avgTls: "common.charts.phases.tls",
+ avgRequest: "common.charts.phases.request",
+ avgFirstByte: "common.charts.phases.firstByte",
+ avgDownload: "common.charts.phases.download",
+};
+
+export const AREA_COLORS = [
+ // Blues
+ "#3182bd", // Deep blue
+ "#6baed6", // Medium blue
+ "#9ecae1", // Light blue
+
+ // Greens
+ "#74c476", // Soft green
+ "#a1d99b", // Light green
+ "#c7e9c0", // Pale green
+
+ // Oranges
+ "#fdae6b", // Warm orange
+ "#fdd0a2", // Light orange
+ "#feedde", // Pale orange
+
+ // Purples
+ "#9467bd", // Lavender
+ "#a55194", // Deep magenta
+ "#c994c7", // Soft magenta
+
+ // Reds
+ "#ff9896", // Soft red
+ "#de2d26", // Deep red
+ "#fc9272", // Medium red
+
+ // Cyans/Teals
+ "#17becf", // Cyan
+ "#7fcdbb", // Teal
+ "#a1dab4", // Light teal
+
+ // Yellows
+ "#fec44f", // Mustard
+ "#fee391", // Light yellow
+ "#ffffd4", // Pale yellow
+
+ // Additional colors
+ "#e377c2", // Soft pink
+ "#bcbd22", // Olive
+ "#2ca02c", // Vibrant green
+];
+
+export const createGradient = ({
+ id,
+ startColor,
+ endColor,
+ startOpacity = 0.8,
+ endOpacity = 0,
+ direction = "vertical",
+}: {
+ id: string;
+ startColor: string;
+ endColor: string;
+ startOpacity?: number;
+ endOpacity?: number;
+ direction?: "vertical" | "horizontal";
+}) => (
+
+
+
+
+
+
+);
diff --git a/client/src/Components/monitors/charts/HistogramDockerContainer.tsx b/client/src/Components/monitors/charts/HistogramDockerContainer.tsx
new file mode 100644
index 000000000..b7a420fc9
--- /dev/null
+++ b/client/src/Components/monitors/charts/HistogramDockerContainer.tsx
@@ -0,0 +1,70 @@
+import { BaseChart } from "@/Components/design-elements";
+import { Area, AreaChart, CartesianGrid, ResponsiveContainer } from "recharts";
+
+// Types
+import type { DockerContainerStats } from "@/Types/Monitor";
+
+// Hooks
+import { useTheme } from "@mui/material";
+import { Fragment, useId } from "react";
+
+// Utils
+import { createGradient } from "@/Components/monitors/charts/ChartUtils";
+interface HistogramDockerContainerProps {
+ title: string;
+ rightTitle: string;
+ stats: DockerContainerStats;
+ dataKey: string;
+ strokeColor: string;
+ gradientStartColor: string;
+}
+
+export const HistogramDockerContainer = ({
+ title,
+ rightTitle,
+ stats,
+ dataKey,
+ strokeColor,
+ gradientStartColor,
+}: HistogramDockerContainerProps) => {
+ const uniqueId = useId();
+ const theme = useTheme();
+ const gradientId = `gradient-${uniqueId}`;
+
+ return (
+
+
+
+
+
+ {createGradient({
+ id: gradientId,
+ startColor: gradientStartColor,
+ endColor: "transparent",
+ direction: "vertical",
+ })}
+
+
+
+
+
+ );
+};
diff --git a/client/src/Components/monitors/charts/HistogramInfrastructure.tsx b/client/src/Components/monitors/charts/HistogramInfrastructure.tsx
index 8163898aa..bd3151364 100644
--- a/client/src/Components/monitors/charts/HistogramInfrastructure.tsx
+++ b/client/src/Components/monitors/charts/HistogramInfrastructure.tsx
@@ -12,85 +12,7 @@ import { XTick } from "@/Components/monitors/charts/XTick";
import { useTheme } from "@mui/material/styles";
import type { HardwareCheckStats } from "@/Types/Monitor";
-
-const AREA_COLORS = [
- // Blues
- "#3182bd", // Deep blue
- "#6baed6", // Medium blue
- "#9ecae1", // Light blue
-
- // Greens
- "#74c476", // Soft green
- "#a1d99b", // Light green
- "#c7e9c0", // Pale green
-
- // Oranges
- "#fdae6b", // Warm orange
- "#fdd0a2", // Light orange
- "#feedde", // Pale orange
-
- // Purples
- "#9467bd", // Lavender
- "#a55194", // Deep magenta
- "#c994c7", // Soft magenta
-
- // Reds
- "#ff9896", // Soft red
- "#de2d26", // Deep red
- "#fc9272", // Medium red
-
- // Cyans/Teals
- "#17becf", // Cyan
- "#7fcdbb", // Teal
- "#a1dab4", // Light teal
-
- // Yellows
- "#fec44f", // Mustard
- "#fee391", // Light yellow
- "#ffffd4", // Pale yellow
-
- // Additional colors
- "#e377c2", // Soft pink
- "#bcbd22", // Olive
- "#2ca02c", // Vibrant green
-];
-
-const createGradient = ({
- id,
- startColor,
- endColor,
- startOpacity = 0.8,
- endOpacity = 0,
- direction = "vertical",
-}: {
- id: string;
- startColor: string;
- endColor: string;
- startOpacity?: number;
- endOpacity?: number;
- direction?: "vertical" | "horizontal";
-}) => (
-
-
-
-
-
-
-);
+import { AREA_COLORS, createGradient } from "@/Components/monitors/charts/ChartUtils";
export const HistogramInfrastructure = ({
dateRange,
diff --git a/client/src/Pages/Docker/Details/Components/Charts.tsx b/client/src/Pages/Docker/Details/Components/Charts.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/client/src/Pages/Docker/Details/Components/StatBoxes.tsx b/client/src/Pages/Docker/Details/Components/StatBoxes.tsx
new file mode 100644
index 000000000..79bf89e16
--- /dev/null
+++ b/client/src/Pages/Docker/Details/Components/StatBoxes.tsx
@@ -0,0 +1,54 @@
+import Stack from "@mui/material/Stack";
+import { StatBox } from "@/Components/design-elements";
+
+// Types
+import type { Monitor } from "@/Types/Monitor";
+
+// Hooks
+import { useTheme } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import type { DockerContainerInfo } from "@/Types/Check";
+
+// Utils
+import { formatDuration } from "@/Utils/TimeUtils";
+
+export const DockerContainerStatusBoxes = ({
+ monitor,
+ container,
+}: {
+ monitor: Monitor | undefined;
+ container: DockerContainerInfo | undefined;
+}) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ if (!container) return null;
+
+ const { state, restartCount, health } = container;
+ const startedAt = container.startedAt ?? "0";
+ const runningFor = Date.now() - new Date(startedAt).getTime();
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/Docker/Details/TabLogs.tsx b/client/src/Pages/Docker/Details/TabLogs.tsx
new file mode 100644
index 000000000..9dcea4842
--- /dev/null
+++ b/client/src/Pages/Docker/Details/TabLogs.tsx
@@ -0,0 +1,3 @@
+export const TabLogs = () => {
+ return "This is the logs tab";
+};
diff --git a/client/src/Pages/Docker/Details/TabOverview.tsx b/client/src/Pages/Docker/Details/TabOverview.tsx
new file mode 100644
index 000000000..ce746cc19
--- /dev/null
+++ b/client/src/Pages/Docker/Details/TabOverview.tsx
@@ -0,0 +1,62 @@
+// Components
+import Grid from "@mui/material/Grid";
+import { HistogramDockerContainer } from "@/Components/monitors/charts/HistogramDockerContainer";
+
+// Hooks
+import { useTheme } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import useMediaQuery from "@mui/material/useMediaQuery";
+
+// Types
+import type { Theme } from "@mui/material";
+import type { TFunction } from "i18next";
+import type { DockerContainerStats } from "@/Types/Monitor";
+import { LAYOUT } from "@/Utils/Theme/constants";
+import { formatPercentage } from "@/Utils/FormatUtils";
+import prettyBytes from "pretty-bytes";
+
+const getChartConfigs = (theme: Theme, t: TFunction, stats: DockerContainerStats) => [
+ {
+ title: t("common.charts.labels.cpuUsage"),
+ key: "avgCpuPct",
+ color: theme.palette.success.main,
+ rightTitle: formatPercentage(stats?.latest?.container.cpuPct || 0),
+ },
+ {
+ title: t("common.charts.labels.memoryUsage"),
+ key: "avgMemoryUsedBytes",
+ color: theme.palette.primary.main,
+ rightTitle: prettyBytes(stats?.latest?.container.memoryUsedBytes || 0),
+ },
+];
+
+export const TabOverview = ({ stats }: { stats: DockerContainerStats }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+
+ return (
+
+ {getChartConfigs(theme, t, stats).map((config) => {
+ return (
+
+
+
+ );
+ })}
+
+ );
+};
diff --git a/client/src/Pages/Docker/Details/index.tsx b/client/src/Pages/Docker/Details/index.tsx
index 5937e113e..33ef7da9e 100644
--- a/client/src/Pages/Docker/Details/index.tsx
+++ b/client/src/Pages/Docker/Details/index.tsx
@@ -1,5 +1,75 @@
-const DockerDetailsPage = () => {
- return null;
+// Components
+import { BasePage, Tabs, Tab, DockerStateLabel } from "@/Components/design-elements";
+import { TabOverview } from "@/Pages/Docker/Details/TabOverview";
+import { TabLogs } from "@/Pages/Docker/Details/TabLogs";
+
+// Hooks
+import { useParams } from "react-router-dom";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useGet } from "@/Hooks/UseApi";
+import type { DockerContainerResponse } from "@/Types/Monitor";
+import { DockerContainerStatusBoxes } from "@/Pages/Docker/Details/Components/StatBoxes";
+interface DockerContainerDetailsProps {
+ open?: "overview" | "logs";
+}
+
+const TAB_MAP = {
+ overview: 0,
+ logs: 1,
+} as const;
+
+const DockerDetailsPage = ({ open = "overview" }: DockerContainerDetailsProps) => {
+ const [activeTab, setActiveTab] = useState(TAB_MAP[open]);
+ const { monitorId, containerName } = useParams<{
+ monitorId: string;
+ containerName: string;
+ }>();
+ const { t } = useTranslation();
+
+ const monitorDetailsUrl = monitorId
+ ? `/monitors/docker/details/${monitorId}/containers/${containerName}`
+ : null;
+
+ const { data: containerData, refetch: refetchContainer } =
+ useGet(
+ monitorDetailsUrl,
+ {},
+ { refreshInterval: 10000, keepPreviousData: true }
+ );
+
+ const monitor = containerData?.monitor;
+ if (!monitor) return null;
+
+ const stats = containerData.stats;
+
+ const container = stats?.latest?.container;
+ if (!container) return null;
+
+ const { state, name, image } = container;
+
+ return (
+
+
+
+ setActiveTab(newValue)}
+ >
+
+
+
+ {activeTab === 0 && }
+ {activeTab === 1 && }
+
+ );
};
export default DockerDetailsPage;
diff --git a/client/src/Pages/Docker/HostDetails/Components/DockerContainersTable.tsx b/client/src/Pages/Docker/HostDetails/Components/DockerContainersTable.tsx
index bf5bdc216..72d828f45 100644
--- a/client/src/Pages/Docker/HostDetails/Components/DockerContainersTable.tsx
+++ b/client/src/Pages/Docker/HostDetails/Components/DockerContainersTable.tsx
@@ -15,11 +15,15 @@ import type { DockerContainerInfo } from "@/Types/Check";
import { formatPercentage } from "@/Utils/FormatUtils";
interface DockerContainersTableProps {
+ monitorId: string | undefined;
containers: DockerContainerInfo[];
onRowClick?: (container: DockerContainerInfo) => void;
}
-export const DockerContainersTable = ({ containers }: DockerContainersTableProps) => {
+export const DockerContainersTable = ({
+ monitorId,
+ containers,
+}: DockerContainersTableProps) => {
const { t } = useTranslation();
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
@@ -64,7 +68,7 @@ export const DockerContainersTable = ({ containers }: DockerContainersTableProps
},
{
id: "health",
- content: t("pages.docker.host.table.headers.health"),
+ content: t("common.labels.health"),
render: (row) => {
return {row.health};
},
@@ -85,7 +89,7 @@ export const DockerContainersTable = ({ containers }: DockerContainersTableProps
},
{
id: "restarts",
- content: t("pages.docker.host.table.headers.restarts"),
+ content: t("common.labels.restarts"),
render: (row) => {
return {row.restartCount};
},
@@ -100,14 +104,13 @@ export const DockerContainersTable = ({ containers }: DockerContainersTableProps
headers = headers.filter((h) => h.id !== "histogram");
}
- console.log(JSON.stringify(containers, null, 2));
return (
{
- navigate(`/docker/container/${row.id}`);
+ navigate(`/docker/host/${monitorId}/container/${row.name}`);
}}
// getRowSx={(row) => ({
// backgroundColor: isRowSelected(row.id)
diff --git a/client/src/Pages/Docker/HostDetails/index.tsx b/client/src/Pages/Docker/HostDetails/index.tsx
index 4cd2d3903..7c2d3c80c 100644
--- a/client/src/Pages/Docker/HostDetails/index.tsx
+++ b/client/src/Pages/Docker/HostDetails/index.tsx
@@ -44,7 +44,10 @@ const DockerHostDetailsPage = () => {
monitorStats={monitorStats}
/>
-
+
);
};
diff --git a/client/src/Pages/Infrastructure/Details/Components/Charts.tsx b/client/src/Pages/Infrastructure/Details/Components/Charts.tsx
index 4309feca7..4ef5fa145 100644
--- a/client/src/Pages/Infrastructure/Details/Components/Charts.tsx
+++ b/client/src/Pages/Infrastructure/Details/Components/Charts.tsx
@@ -12,7 +12,7 @@ const formatTemp = (value: number) => `${value.toFixed(0)}°C`;
const getChartConfigs = (theme: any, checks: HardwareCheckStats[], t: any) => {
return [
{
- title: t("pages.infrastructure.charts.labels.memory"),
+ title: t("common.charts.labels.memoryUsage"),
type: "memory",
dataKeys: ["avgMemoryUsage"],
strokeColor: theme.palette.primary.main,
@@ -22,7 +22,7 @@ const getChartConfigs = (theme: any, checks: HardwareCheckStats[], t: any) => {
idx: null,
},
{
- title: t("pages.infrastructure.charts.labels.cpu"),
+ title: t("common.charts.labels.cpuUsage"),
type: "cpu",
dataKeys: ["avgCpuUsage"],
strokeColor: theme.palette.success.main,
diff --git a/client/src/Routes/index.tsx b/client/src/Routes/index.tsx
index e7735fcd0..a3fccf378 100644
--- a/client/src/Routes/index.tsx
+++ b/client/src/Routes/index.tsx
@@ -157,7 +157,7 @@ const Routes = () => {
element={}
/>
}
/>