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={} /> } />