resolve disk selection persistence and validation issues

This commit is contained in:
ppmar
2025-12-17 23:50:40 +01:00
parent 7edc2a71d8
commit ff2f84d46b
16 changed files with 331 additions and 136 deletions

View File

@@ -91,6 +91,7 @@ Feel free to ask questions or share your ideas - we'd love to hear from you!
- Website monitoring
- Page speed monitoring
- Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent
- Selective disk monitoring with mountpoint selection
- Docker monitoring
- Ping monitoring
- SSL monitoring

View File

@@ -387,6 +387,7 @@ const useUpdateMonitor = () => {
...(monitor.type === "hardware" && {
thresholds: monitor.thresholds,
secret: monitor.secret,
selectedDisks: monitor.selectedDisks,
}),
};
await networkService.updateMonitor({

View File

@@ -11,7 +11,6 @@ const DiskSelection = ({ availableDisks, selectedDisks, onChange }) => {
const { t } = useTranslation();
const handleDiskChange = (event, mountpoint) => {
// Le composant Checkbox personnalisé renvoie l'event natif
const isChecked = event.target.checked;
let newSelectedDisks = [];
@@ -37,42 +36,38 @@ const DiskSelection = ({ availableDisks, selectedDisks, onChange }) => {
<Stack gap={theme.spacing(6)}>
{(!availableDisks || availableDisks.length === 0) ? (
<Typography
variant="body2"
<Typography
variant="body2"
sx={{ fontStyle: 'italic', opacity: 0.8 }}
>
{t("v1.infrastructure.disk_selection_info")}
</Typography>
) : (
availableDisks.map((disk) => (
/* Reproduction exacte de la structure de CustomThreshold
pour garantir l'alignement parfait avec la section Alertes
*/
<Stack
key={disk.mountpoint}
direction={{ sm: "column", md: "row" }}
spacing={theme.spacing(2)}
>
<Box
sx={{
// Mêmes largeurs que dans CustomThreshold pour aligner les labels
width: { md: "45%", lg: "25%", xl: "20%" },
}}
justifyContent="flex-start"
availableDisks.map((disk) => {
const identifier = disk.mountpoint || disk.device;
return (
<Stack
key={identifier}
direction={{ sm: "column", md: "row" }}
spacing={theme.spacing(2)}
>
<Checkbox
id={`disk-${disk.mountpoint}`}
name={disk.mountpoint}
label={disk.mountpoint}
isChecked={selectedDisks.includes(disk.mountpoint)}
onChange={(e) => handleDiskChange(e, disk.mountpoint)}
/>
</Box>
{/* Pas de deuxième Stack ici car nous n'avons pas besoin
du champ TextInput pour la sélection simple
*/}
</Stack>
))
<Box
sx={{
width: "100%",
}}
justifyContent="flex-start"
>
<Checkbox
id={`disk-${identifier}`}
name={identifier}
label={identifier}
isChecked={selectedDisks.includes(identifier)}
onChange={(e) => handleDiskChange(e, identifier)}
/>
</Box>
</Stack>
)
})
)}
</Stack>
</ConfigBox>

View File

@@ -29,11 +29,11 @@ const useInfrastructureSubmit = () => {
...(infrastructureMonitor.disk
? { usage_disk: infrastructureMonitor.usage_disk }
: {}),
temperature: infrastructureMonitor.temperature,
...(infrastructureMonitor.temperature
? { usage_temperature: infrastructureMonitor.usage_temperature }
: {}),
secret: infrastructureMonitor.secret,
selectedDisks: infrastructureMonitor.selectedDisks,
};
return form;
};
@@ -52,6 +52,7 @@ const useInfrastructureSubmit = () => {
usage_disk,
temperature,
usage_temperature,
selectedDisks,
...rest
} = form;
@@ -68,6 +69,7 @@ const useInfrastructureSubmit = () => {
description: form.name,
type: "hardware",
notifications: infrastructureMonitor.notifications,
selectedDisks,
thresholds,
};
// Handle create or update

View File

@@ -14,7 +14,7 @@ import DiskSelection from "./Components/DiskSelection.jsx";
// Utils
import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx";
import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js";
import NetworkService from "../../../../Utils/NetworkService";
import { networkService } from "../../../../Utils/NetworkService";
import { useParams } from "react-router-dom";
import { useState, useEffect } from "react";
import { useTheme } from "@emotion/react";
@@ -75,9 +75,9 @@ const CreateInfrastructureMonitor = () => {
...(isCreate
? [{ name: "Create", path: "/infrastructure/create" }]
: [
{ name: "Details", path: `/infrastructure/${monitorId}` },
{ name: "Configure", path: `/infrastructure/configure/${monitorId}` },
]),
{ name: "Details", path: `/infrastructure/${monitorId}` },
{ name: "Configure", path: `/infrastructure/configure/${monitorId}` },
]),
];
// Populate form fields if editing
useEffect(() => {
@@ -89,23 +89,30 @@ const CreateInfrastructureMonitor = () => {
setHttps(monitor.url.startsWith("https"));
initializeInfrastructureMonitorForUpdate(monitor);
const fetchLastCheck = async () => {
try {
const params = {
dateRange: "recent",
limit: 1,
sortOrder: "desc",
};
// On utilise NetworkService directement
const response = await NetworkService.get(`/api/v1/checks/${monitorId}`, { params });
const disks = response?.data?.checks?.[0]?.payload?.disks || [];
setAvailableDisks(disks);
} catch (error) {
console.error("Erreur pendant la récupération des disques:", error);
setAvailableDisks([]); // En cas d'erreur, on met un tableau vide
}
};
try {
const { stats } = monitor ?? {};
let latestCheck = stats?.aggregateData?.latestCheck;
let disks = latestCheck?.disk || [];
fetchLastCheck();
if (disks.length > 0) {
setAvailableDisks(disks);
return;
}
const response = await networkService.getChecksByMonitor({
monitorId,
dateRange: "all",
rowsPerPage: 1,
sortOrder: "desc",
});
disks = response?.data?.data?.checks?.[0]?.disk || [];
setAvailableDisks(disks);
} catch (error) {
setAvailableDisks([]);
}
};
fetchLastCheck();
}
}, [
isCreate,
@@ -154,14 +161,7 @@ const CreateInfrastructureMonitor = () => {
isPausing ||
notificationsAreLoading;
// --- AJOUT TEMPORAIRE : Fausses données ---
const MOCK_DISKS = [
{ mountpoint: "C:" },
{ mountpoint: "D:" },
{ mountpoint: "/mnt/data" },
{ mountpoint: "/var/lib/docker" }
];
// -----------------------------------------
return (
<Box className="create-infrastructure-monitor">
@@ -359,15 +359,15 @@ const CreateInfrastructureMonitor = () => {
/>
{monitorId && (
<DiskSelection
availableDisks={availableDisks}
selectedDisks={infrastructureMonitor.selectedDisks}
onChange={(newSelectedDisks) =>
onChangeForm("selectedDisks", newSelectedDisks)
}
/>
)}
<DiskSelection
availableDisks={availableDisks}
selectedDisks={infrastructureMonitor.selectedDisks}
onChange={(newSelectedDisks) =>
onChangeForm("selectedDisks", newSelectedDisks)
}
/>
)}
<ConfigBox>
<Box>
<Typography

View File

@@ -46,20 +46,27 @@ const Gauges = ({ isLoading = false, monitor }) => {
metricTwo: t("frequency"),
valueTwo: `${(cpuFrequency / 1000).toFixed(2)} Ghz`,
},
...(latestCheck?.disk ?? []).map((disk, idx) => ({
type: "disk",
diskIndex: idx,
value: decimalToPercentage(disk.usage_percent),
heading: `Disk${idx} usage`,
metricOne: t("used"),
valueOne: formatBytes(disk.total_bytes - disk.free_bytes, true),
metricTwo: t("total"),
valueTwo: formatBytes(disk.total_bytes, true),
metricThree: t("device"),
valueThree: formatDeviceName(disk.device),
metricFour: t("mountpoint"),
valueFour: formatMountpoint(disk.mountpoint),
})),
...(latestCheck?.disk ?? [])
.filter((disk) => {
if (!monitor?.selectedDisks || monitor.selectedDisks.length === 0) {
return true;
}
return monitor.selectedDisks.includes(disk.mountpoint || disk.device);
})
.map((disk, idx) => ({
type: "disk",
diskIndex: idx,
value: decimalToPercentage(disk.usage_percent),
heading: `Disk${idx} usage`,
metricOne: t("used"),
valueOne: formatBytes(disk.total_bytes - disk.free_bytes, true),
metricTwo: t("total"),
valueTwo: formatBytes(disk.total_bytes, true),
metricThree: t("device"),
valueThree: formatDeviceName(disk.device),
metricFour: t("mountpoint"),
valueFour: formatMountpoint(disk.mountpoint),
})),
];
return (

View File

@@ -1,4 +1,4 @@
import { Typography } from "@mui/material";
import { Typography, Tooltip } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
@@ -121,19 +121,84 @@ const useHardwareUtils = () => {
};
const formatDeviceName = (device) => {
const deviceStr = String(device || '');
// Extract the last part of the path (after last '/')
const parts = deviceStr.split('/');
const lastPart = parts[parts.length - 1];
// If there's more than one part, show with "..." prefix
const displayText = parts.length > 1 ? `.../${lastPart}` : deviceStr;
// Always show tooltip with full device path
return (
<>
{String(device)}
</>
<Tooltip title={deviceStr} arrow placement="top">
<Typography
component="span"
sx={{
cursor: 'default',
display: 'inline-block',
userSelect: 'none',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%'
}}
>
{displayText}
</Typography>
</Tooltip>
);
};
const formatMountpoint = (mountpoint) => {
const mountpointStr = String(mountpoint || '');
if (!mountpointStr) {
return (
<Tooltip title="No mountpoint available" arrow placement="top">
<Typography
component="span"
sx={{
cursor: 'default',
display: 'inline-block',
userSelect: 'none',
color: 'text.secondary',
fontStyle: 'italic'
}}
>
N/A
</Typography>
</Tooltip>
);
}
// Extract the last part of the path (after last '/')
const parts = mountpointStr.split('/');
const lastPart = parts[parts.length - 1];
// If there's more than one part, show with "..." prefix
const displayText = parts.length > 1 ? `.../${lastPart}` : mountpointStr;
// Always show tooltip with full mountpoint path
return (
<>
{String(mountpoint)}
</>
)
<Tooltip title={mountpointStr} arrow placement="top">
<Typography
component="span"
sx={{
cursor: 'default',
display: 'inline-block',
userSelect: 'none',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%'
}}
>
{displayText}
</Typography>
</Tooltip>
);
}
/**

View File

@@ -144,44 +144,44 @@ const monitorValidation = joi.object({
// Regex from https://gist.github.com/dperini/729294
var urlRegex = new RegExp(
"^" +
// protocol identifier (optional)
// short syntax // still required
"(?:(?:https?|ftp):\\/\\/)?" +
// user:pass BasicAuth (optional)
"(?:" +
// IP address dotted notation octets
// excludes loopback network 0.0.0.0
// excludes reserved space >= 224.0.0.0
// excludes network & broadcast addresses
// (first & last IP address of each class)
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
"|" +
// host & domain names, may end with dot
// can be replaced by a shortest alternative
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
"(?:" +
// Single hostname without dots (like localhost)
"[a-z0-9\\u00a1-\\uffff][a-z0-9\\u00a1-\\uffff_-]{0,62}" +
"|" +
// Domain with dots
"(?:" +
"(?:" +
"[a-z0-9\\u00a1-\\uffff]" +
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
")?" +
"[a-z0-9\\u00a1-\\uffff]\\." +
")+" +
// TLD identifier name, may end with dot
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
")" +
")" +
// port number (optional)
"(?::\\d{2,5})?" +
// resource path (optional)
"(?:[/?#]\\S*)?" +
"$",
// protocol identifier (optional)
// short syntax // still required
"(?:(?:https?|ftp):\\/\\/)?" +
// user:pass BasicAuth (optional)
"(?:" +
// IP address dotted notation octets
// excludes loopback network 0.0.0.0
// excludes reserved space >= 224.0.0.0
// excludes network & broadcast addresses
// (first & last IP address of each class)
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
"|" +
// host & domain names, may end with dot
// can be replaced by a shortest alternative
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
"(?:" +
// Single hostname without dots (like localhost)
"[a-z0-9\\u00a1-\\uffff][a-z0-9\\u00a1-\\uffff_-]{0,62}" +
"|" +
// Domain with dots
"(?:" +
"(?:" +
"[a-z0-9\\u00a1-\\uffff]" +
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
")?" +
"[a-z0-9\\u00a1-\\uffff]\\." +
")+" +
// TLD identifier name, may end with dot
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
")" +
")" +
// port number (optional)
"(?::\\d{2,5})?" +
// resource path (optional)
"(?:[/?#]\\S*)?" +
"$",
"i"
);
if (!urlRegex.test(value)) {
@@ -448,6 +448,7 @@ const infrastructureMonitorValidation = joi.object({
"number.max": "Status window threshold cannot exceed 100%.",
}),
notifications: joi.array().items(joi.string()),
selectedDisks: joi.array().items(joi.string()).optional(),
});
const notificationValidation = joi.object({

View File

@@ -1129,5 +1129,12 @@
"packetsReceivedRate": "Rate empfangener Pakete",
"packetsSent": "Gesendete Pakete",
"rate": "Rate",
"selectInterface": "Schnittstelle auswählen"
"selectInterface": "Schnittstelle auswählen",
"v1": {
"infrastructure": {
"disk_selection_title": "Festplattenauswahl",
"disk_selection_info": "Momentan wurde keine Festplatte erkannt.",
"disk_selection_description": "Wählen Sie die spezifischen Festplatten aus, die Sie überwachen möchten."
}
}
}

View File

@@ -1129,5 +1129,12 @@
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
"selectInterface": "",
"v1": {
"infrastructure": {
"disk_selection_title": "Sélection des disques",
"disk_selection_info": "Aucun disque détecté pour le moment.",
"disk_selection_description": "Sélectionnez les disques spécifiques que vous souhaitez surveiller."
}
}
}

View File

@@ -1108,5 +1108,12 @@
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
"selectInterface": "",
"v1": {
"infrastructure": {
"disk_selection_title": "ディスク選択",
"disk_selection_info": "現在ディスクが検出されていません。",
"disk_selection_description": "監視したい特定のディスクを選択してください。"
}
}
}

View File

@@ -1129,5 +1129,12 @@
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
"selectInterface": "",
"v1": {
"infrastructure": {
"disk_selection_title": "Seleção de Discos",
"disk_selection_info": "Nenhum disco detectado no momento.",
"disk_selection_description": "Selecione os discos específicos que você deseja monitorar."
}
}
}

View File

@@ -1129,5 +1129,12 @@
"packetsReceivedRate": "Скорость получения пакетов",
"packetsSent": "Отправлено пакетов",
"rate": "коэффициент",
"selectInterface": "Выберите интерфейс"
"selectInterface": "Выберите интерфейс",
"v1": {
"infrastructure": {
"disk_selection_title": "Выбор дисков",
"disk_selection_info": "На данный момент диск не обнаружен.",
"disk_selection_description": "Выберите конкретные диски, которые вы хотите отслеживать."
}
}
}

View File

@@ -1129,5 +1129,12 @@
"packetsReceivedRate": "อัตราการรับแพ็กเก็ต",
"packetsSent": "แพ็กเก็ตที่ส่ง",
"rate": "อัตรา",
"selectInterface": "เลือกอินเทอร์เฟซ"
"selectInterface": "เลือกอินเทอร์เฟซ",
"v1": {
"infrastructure": {
"disk_selection_title": "การเลือกดิสก์",
"disk_selection_info": "ไม่พบดิสก์ในขณะนี้",
"disk_selection_description": "เลือกดิสก์ที่คุณต้องการตรวจสอบ"
}
}
}

View File

@@ -1121,5 +1121,12 @@
"packetsReceivedRate": "数据包接收率",
"packetsSent": "已发送数据包",
"rate": "速度",
"selectInterface": "选择接口"
"selectInterface": "选择接口",
"v1": {
"infrastructure": {
"disk_selection_title": "磁盘选择",
"disk_selection_info": "目前没有检测到磁盘。",
"disk_selection_description": "选择您要监控的特定磁盘。"
}
}
}

View File

@@ -0,0 +1,74 @@
# Infrastructure Monitoring - Disk Selection
This guide explains how to use the selective disk monitoring feature in Checkmate's infrastructure monitoring.
## Overview
By default, Checkmate monitors all detected disks on your server. The disk selection feature allows you to choose specific disks/mountpoints to monitor, giving you more control over what gets tracked and displayed.
## Prerequisites
- Checkmate server running with [Capture agent](https://github.com/bluewave-labs/capture) installed on target server
- An existing infrastructure monitor already created and configured
- At least one disk detected by the Capture agent
## Using Disk Selection
### Accessing the Feature
1. Navigate to your Infrastructure monitors page
2. Click on an existing infrastructure monitor to view details
3. Click the "Configure" or "Edit" button
4. Scroll down to the **Disk Selection** section
> **Note:** The disk selection feature is only available when editing existing monitors that have already detected disks.
### Selecting Disks to Monitor
1. **View Available Disks**: The system automatically detects all disks/mountpoints from your server
2. **Select Disks**: Check the boxes next to the disks you want to monitor
3. **Apply Changes**: Click "Save" to update your monitor configuration
### Understanding Disk Identifiers
Disks are identified by their mountpoint or device name:
- **Mountpoint**: `/` (root), `/home`, `/var`, etc.
- **Device**: `/dev/sda1`, `/dev/nvme0n1p1`, etc.
### Behavior
- **All disks selected**: Shows all detected disks in gauges and charts
- **Specific disks selected**: Only shows selected disks in the monitoring interface
- **No disks detected**: Displays "No disk detected for the moment" message
## What Gets Filtered
When you select specific disks, the following elements are filtered:
- **Disk Usage Gauges**: Only selected disks appear in the dashboard
- **Disk Usage Charts**: Time-series charts show only selected disks
- **Monitor Table**: Summary view reflects only selected disk usage
## Example Use Cases
- **Server with multiple drives**: Monitor only critical system disks, ignore temporary storage
- **Database servers**: Focus monitoring on database partition while ignoring logs partition
- **Web servers**: Monitor web content disk separately from system disk
- **Development environments**: Track only project-specific mountpoints
## Troubleshooting
### No Disks Appearing
- Verify the Capture agent is running on the target server
- Check that the agent has proper permissions to read disk information
- Ensure the monitor has been running long enough to collect at least one check
### Missing Disk Information
- Some disks may not be accessible to the Capture agent due to permissions
- Virtual or network-mounted disks may not appear in the list
- Check the Capture agent logs for any disk detection errors
## Related Documentation
- [Infrastructure Monitoring Setup](https://docs.checkmate.so/checkmate-2.1)
- [Capture Agent Installation](https://github.com/bluewave-labs/capture)