resolve disk selection persistence and validation issues

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

No files matched your search

+1
View File
@@ -387,6 +387,7 @@ const useUpdateMonitor = () => {
...(monitor.type === "hardware" && {
thresholds: monitor.thresholds,
secret: monitor.secret,
selectedDisks: monitor.selectedDisks,
}),
};
await networkService.updateMonitor({
@@ -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>
@@ -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
@@ -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
@@ -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 (
@@ -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>
);
}
/**
+39 -38
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({
+8 -1
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."
}
}
}
+8 -1
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."
}
}
}
+8 -1
View File
@@ -1108,5 +1108,12 @@
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
"selectInterface": "",
"v1": {
"infrastructure": {
"disk_selection_title": "ディスク選択",
"disk_selection_info": "現在ディスクが検出されていません。",
"disk_selection_description": "監視したい特定のディスクを選択してください。"
}
}
}
+8 -1
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."
}
}
}
+8 -1
View File
@@ -1129,5 +1129,12 @@
"packetsReceivedRate": "Скорость получения пакетов",
"packetsSent": "Отправлено пакетов",
"rate": "коэффициент",
"selectInterface": "Выберите интерфейс"
"selectInterface": "Выберите интерфейс",
"v1": {
"infrastructure": {
"disk_selection_title": "Выбор дисков",
"disk_selection_info": "На данный момент диск не обнаружен.",
"disk_selection_description": "Выберите конкретные диски, которые вы хотите отслеживать."
}
}
}
+8 -1
View File
@@ -1129,5 +1129,12 @@
"packetsReceivedRate": "อัตราการรับแพ็กเก็ต",
"packetsSent": "แพ็กเก็ตที่ส่ง",
"rate": "อัตรา",
"selectInterface": "เลือกอินเทอร์เฟซ"
"selectInterface": "เลือกอินเทอร์เฟซ",
"v1": {
"infrastructure": {
"disk_selection_title": "การเลือกดิสก์",
"disk_selection_info": "ไม่พบดิสก์ในขณะนี้",
"disk_selection_description": "เลือกดิสก์ที่คุณต้องการตรวจสอบ"
}
}
}
+8 -1
View File
@@ -1121,5 +1121,12 @@
"packetsReceivedRate": "数据包接收率",
"packetsSent": "已发送数据包",
"rate": "速度",
"selectInterface": "选择接口"
"selectInterface": "选择接口",
"v1": {
"infrastructure": {
"disk_selection_title": "磁盘选择",
"disk_selection_info": "目前没有检测到磁盘。",
"disk_selection_description": "选择您要监控的特定磁盘。"
}
}
}