## Summary Add support for monitoring Proxmox LXC containers through the Capture agent's /api/v1/metrics/proxmox endpoint. This feature is opt-in via a new setting in the infrastructure monitor configuration. ## Backend Changes - Add enableProxmoxContainers field to Monitor schema and types - Update networkService to conditionally fetch Proxmox metrics - Handle HTTP 207 multi-status responses and merge errors - Add containers field to Check schema for storing container data - Update validation schemas for the new setting ## Frontend Changes - Add ContainerStats component with charts and stat boxes - Add Containers tab to infrastructure monitor details (conditional) - Add enableProxmoxContainers switch in monitor create/edit form - Fix chart colors for disk I/O and network I/O to be distinct - Add dotColor2 prop to InfrastructureTooltip for multi-series charts - Add translation keys to all 16 locale files ## How It Works 1. User enables "Proxmox container monitoring" in monitor settings 2. Checkmate appends /proxmox to the metrics URL when fetching 3. Container data is displayed in a dedicated tab with: - CPU, memory, disk, swap, and uptime stat boxes - Interactive charts for CPU, memory, disk I/O, network I/O 4. Errors from Capture are merged and logged appropriately
126 lines
3.2 KiB
React
126 lines
3.2 KiB
React
import PropTypes from "prop-types";
|
|
import { useState, useEffect } from "react";
|
|
import { Box, Typography } from "@mui/material";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useTheme } from "@emotion/react";
|
|
import Select from "@/Components/v1/Inputs/Select/index.jsx";
|
|
import ContainerStatBoxes from "./ContainerStatBoxes.jsx";
|
|
import ContainerCharts from "./ContainerCharts.jsx";
|
|
import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
|
|
|
|
const getAvailableContainers = (containers) => {
|
|
return (containers || [])
|
|
.filter((c) => c.vmid && c.name)
|
|
.map((c) => ({
|
|
vmid: String(c.vmid),
|
|
name: c.name,
|
|
node: c.node,
|
|
status: c.status,
|
|
type: c.type,
|
|
}));
|
|
};
|
|
|
|
const getContainerData = (checks, vmid) => {
|
|
if (!vmid) return [];
|
|
|
|
return (checks || [])
|
|
.map((check) => {
|
|
const container = (check.containers || []).find(
|
|
(c) => String(c.vmid) === String(vmid)
|
|
);
|
|
if (!container) return null;
|
|
return {
|
|
_id: check._id,
|
|
cpuUsage: container.avgCpuUsage ?? 0,
|
|
memoryUsage: container.avgMemoryUsage ?? 0,
|
|
diskRead: container.deltaDiskRead ?? 0,
|
|
diskWrite: container.deltaDiskWrite ?? 0,
|
|
netIn: container.deltaNetIn ?? 0,
|
|
netOut: container.deltaNetOut ?? 0,
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
};
|
|
|
|
const ContainerStats = ({ containers, checks, isLoading, dateRange, setDateRange }) => {
|
|
const { t } = useTranslation();
|
|
const theme = useTheme();
|
|
|
|
const availableContainers = getAvailableContainers(containers);
|
|
const [selectedContainer, setSelectedContainer] = useState("");
|
|
|
|
useEffect(() => {
|
|
if (availableContainers.length > 0 && !selectedContainer) {
|
|
setSelectedContainer(availableContainers[0].vmid);
|
|
}
|
|
}, [availableContainers, selectedContainer]);
|
|
|
|
const containerData = getContainerData(checks, selectedContainer);
|
|
const currentContainer = containers?.find(
|
|
(c) => String(c.vmid) === String(selectedContainer)
|
|
);
|
|
|
|
if (!containers?.length) {
|
|
return (
|
|
<Box
|
|
display="flex"
|
|
justifyContent="center"
|
|
alignItems="center"
|
|
minHeight={200}
|
|
>
|
|
<Typography color={theme.palette.primary.contrastTextTertiary}>
|
|
{t("noContainersAvailable")}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<ContainerStatBoxes
|
|
shouldRender={!isLoading}
|
|
container={currentContainer}
|
|
/>
|
|
<Box
|
|
display="flex"
|
|
justifyContent="space-between"
|
|
alignItems="flex-end"
|
|
gap={theme.spacing(4)}
|
|
>
|
|
{availableContainers.length > 0 && (
|
|
<Select
|
|
name="container"
|
|
label={t("container")}
|
|
value={selectedContainer}
|
|
onChange={(e) => setSelectedContainer(e.target.value)}
|
|
items={availableContainers.map((c) => ({
|
|
_id: c.vmid,
|
|
name: `${c.name} (${c.vmid}) - ${c.node}`,
|
|
}))}
|
|
sx={{ minWidth: 250 }}
|
|
/>
|
|
)}
|
|
<MonitorTimeFrameHeader
|
|
isLoading={isLoading}
|
|
dateRange={dateRange}
|
|
setDateRange={setDateRange}
|
|
/>
|
|
</Box>
|
|
<ContainerCharts
|
|
containerData={containerData}
|
|
dateRange={dateRange}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
ContainerStats.propTypes = {
|
|
containers: PropTypes.array,
|
|
checks: PropTypes.array,
|
|
isLoading: PropTypes.bool.isRequired,
|
|
dateRange: PropTypes.string.isRequired,
|
|
setDateRange: PropTypes.func.isRequired,
|
|
};
|
|
|
|
export default ContainerStats;
|