Critical fixes: 1. Monitor type detection on edit - When editing a Docker monitor, type is now correctly detected from monitor object - Previously defaulted to "hardware" causing incorrect form fields to show - Fix: Detect type from query params on create, from monitor.type on edit 2. Default interval for Docker monitors - Docker monitors now default to 30 seconds (0.5 minutes) on creation - Hardware monitors remain at 15 seconds (0.25 minutes) - Pass monitorType to initializeInfrastructureMonitorForCreate 3. Type-aware fallback links - Hardware tab empty state: links to /infrastructure/create?type=hardware - Docker tab empty state: links to /infrastructure/create?type=docker - Ensures correct form is shown when creating from empty state UX improvements: 4. Enhanced error messaging in Docker container table - Differentiate between "No containers" vs "Docker daemon unavailable" - Show error details when Capture API fails - Display critical errors (docker.client) with red background - Show partial error messages when some containers fail to inspect 5. Display Capture metadata - Show Capture version in container table header - Display error count when errors are present (⚠ N error(s) encountered) - Help users identify API issues vs legitimate empty states 6. Improved empty state handling - "Docker daemon unavailable" - when Docker socket unreachable on Capture server - "No containers found" - when Capture is working but no containers exist - "Partial data available" - when some containers failed to inspect - Show specific error messages from Capture API These changes improve the user experience when: - Editing existing Docker monitors (correct form fields shown) - Creating Docker monitors (proper default interval) - Troubleshooting Capture API issues (clear error messages) - Understanding container availability (version info, error details)
408 lines
12 KiB
React
408 lines
12 KiB
React
//Components
|
|
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
|
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
|
|
import FieldWrapper from "@/Components/v1/Inputs/FieldWrapper/index.jsx";
|
|
import Link from "@/Components/v1/Link/index.jsx";
|
|
import Select from "@/Components/v1/Inputs/Select/index.jsx";
|
|
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
|
|
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
|
|
import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
|
|
import MonitorStatusHeader from "./Components/MonitorStatusHeader.jsx";
|
|
import MonitorActionButtons from "./Components/MonitorActionButtons.jsx";
|
|
import CustomAlertsSection from "./Components/CustomAlertsSection.jsx";
|
|
// Utils
|
|
import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx";
|
|
import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js";
|
|
import { useParams, useSearchParams } from "react-router-dom";
|
|
import { useState, useEffect } from "react";
|
|
import { useTheme } from "@emotion/react";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
useDeleteMonitor,
|
|
useFetchGlobalSettings,
|
|
useFetchHardwareMonitorById,
|
|
usePauseMonitor,
|
|
} from "../../../../Hooks/v1/monitorHooks.js";
|
|
import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm.jsx";
|
|
import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm.jsx";
|
|
import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx";
|
|
|
|
const CreateInfrastructureMonitor = () => {
|
|
const { monitorId } = useParams();
|
|
const [searchParams] = useSearchParams();
|
|
const isCreate = typeof monitorId === "undefined";
|
|
|
|
const theme = useTheme();
|
|
const { t } = useTranslation();
|
|
|
|
// State
|
|
const [https, setHttps] = useState(false);
|
|
const [updateTrigger, setUpdateTrigger] = useState(false);
|
|
|
|
// Fetch monitor details if editing
|
|
const [monitor, isLoading] = useFetchHardwareMonitorById({
|
|
monitorId,
|
|
updateTrigger,
|
|
});
|
|
|
|
// Determine monitor type: from query params when creating, from monitor object when editing
|
|
const monitorType = isCreate
|
|
? (searchParams.get("type") || "hardware")
|
|
: (monitor?.type || "hardware");
|
|
const [deleteMonitor, isDeleting] = useDeleteMonitor();
|
|
const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings();
|
|
const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId();
|
|
const [pauseMonitor, isPausing] = usePauseMonitor();
|
|
const {
|
|
infrastructureMonitor,
|
|
setInfrastructureMonitor,
|
|
onChangeForm,
|
|
handleCheckboxChange,
|
|
initializeInfrastructureMonitorForCreate,
|
|
initializeInfrastructureMonitorForUpdate,
|
|
} = useInfrastructureMonitorForm();
|
|
const { errors, validateField, validateForm } = useValidateInfrastructureForm();
|
|
const { buildForm, submitInfrastructureForm, isCreating, isUpdating } =
|
|
useInfrastructureSubmit();
|
|
|
|
const ALL_FREQUENCIES = [
|
|
{ _id: 0.25, name: t("time.fifteenSeconds") },
|
|
{ _id: 0.5, name: t("time.thirtySeconds") },
|
|
{ _id: 1, name: t("time.oneMinute") },
|
|
{ _id: 2, name: t("time.twoMinutes") },
|
|
{ _id: 5, name: t("time.fiveMinutes") },
|
|
{ _id: 10, name: t("time.tenMinutes") },
|
|
];
|
|
|
|
// Docker monitors require minimum 30 second interval
|
|
const FREQUENCIES =
|
|
monitorType === "docker"
|
|
? ALL_FREQUENCIES.filter((f) => f._id >= 0.5)
|
|
: ALL_FREQUENCIES;
|
|
const CRUMBS = [
|
|
{ name: "Infrastructure monitors", path: "/infrastructure" },
|
|
...(isCreate
|
|
? [{ name: "Create", path: "/infrastructure/create" }]
|
|
: [
|
|
{ name: "Details", path: `/infrastructure/${monitorId}` },
|
|
{ name: "Configure", path: `/infrastructure/configure/${monitorId}` },
|
|
]),
|
|
];
|
|
// Populate form fields if editing
|
|
useEffect(() => {
|
|
if (isCreate) {
|
|
if (globalSettingsLoading) return;
|
|
setHttps(false);
|
|
const typeFromQuery = searchParams.get("type") || "hardware";
|
|
initializeInfrastructureMonitorForCreate(globalSettings, typeFromQuery);
|
|
} else if (monitor) {
|
|
setHttps(monitor.url.startsWith("https"));
|
|
initializeInfrastructureMonitorForUpdate(monitor);
|
|
}
|
|
}, [
|
|
isCreate,
|
|
monitor,
|
|
globalSettings,
|
|
globalSettingsLoading,
|
|
searchParams,
|
|
initializeInfrastructureMonitorForCreate,
|
|
initializeInfrastructureMonitorForUpdate,
|
|
]);
|
|
|
|
// Handlers
|
|
const onSubmit = async (event) => {
|
|
event.preventDefault();
|
|
const form = buildForm(infrastructureMonitor, https, monitorType);
|
|
const error = validateForm(form);
|
|
if (error) {
|
|
return;
|
|
}
|
|
submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId, monitorType);
|
|
};
|
|
|
|
const triggerUpdate = () => {
|
|
setUpdateTrigger(!updateTrigger);
|
|
};
|
|
|
|
const onChange = (event) => {
|
|
const { value, name } = event.target;
|
|
onChangeForm(name, value);
|
|
validateField(name, value);
|
|
};
|
|
|
|
const handlePause = async () => {
|
|
await pauseMonitor({ monitorId, triggerUpdate });
|
|
};
|
|
|
|
const handleRemove = async (event) => {
|
|
event.preventDefault();
|
|
await deleteMonitor({ monitor, redirect: "/infrastructure" });
|
|
};
|
|
|
|
const isBusy =
|
|
isLoading ||
|
|
isUpdating ||
|
|
isCreating ||
|
|
isDeleting ||
|
|
isPausing ||
|
|
notificationsAreLoading;
|
|
|
|
return (
|
|
<Box className="create-infrastructure-monitor">
|
|
<Breadcrumbs list={CRUMBS} />
|
|
<Stack
|
|
component="form"
|
|
onSubmit={onSubmit}
|
|
noValidate
|
|
spellCheck="false"
|
|
gap={theme.spacing(12)}
|
|
mt={theme.spacing(6)}
|
|
>
|
|
<Stack
|
|
direction="row"
|
|
gap={theme.spacing(2)}
|
|
>
|
|
<Box>
|
|
<Typography
|
|
component="h1"
|
|
variant="h1"
|
|
>
|
|
<Typography
|
|
component="span"
|
|
fontSize="inherit"
|
|
color={
|
|
!isCreate ? theme.palette.primary.contrastTextSecondary : undefined
|
|
}
|
|
>
|
|
{!isCreate ? infrastructureMonitor.name : t("createYour") + " "}
|
|
</Typography>
|
|
{isCreate ? (
|
|
<Typography
|
|
component="span"
|
|
fontSize="inherit"
|
|
fontWeight="inherit"
|
|
color={theme.palette.primary.contrastTextSecondary}
|
|
>
|
|
{t("monitor")}
|
|
</Typography>
|
|
) : (
|
|
<></>
|
|
)}
|
|
</Typography>
|
|
{!isCreate && (
|
|
<MonitorStatusHeader
|
|
monitor={monitor}
|
|
infrastructureMonitor={infrastructureMonitor}
|
|
/>
|
|
)}
|
|
</Box>
|
|
{!isCreate && (
|
|
<MonitorActionButtons
|
|
monitor={monitor}
|
|
isBusy={isBusy}
|
|
handlePause={handlePause}
|
|
handleRemove={handleRemove}
|
|
/>
|
|
)}
|
|
</Stack>
|
|
<ConfigBox>
|
|
<Stack>
|
|
<Typography
|
|
component="h2"
|
|
variant="h2"
|
|
>
|
|
{t("settingsGeneralSettings")}
|
|
</Typography>
|
|
<Typography component="p">
|
|
{monitorType === "docker"
|
|
? "Configure your Capture server to monitor Docker containers."
|
|
: t("infrastructureCreateGeneralSettingsDescription")}
|
|
</Typography>
|
|
<Typography component="p">
|
|
{monitorType === "docker" ? (
|
|
<>
|
|
Capture server endpoint: <code>/api/v1/metrics/docker</code>
|
|
</>
|
|
) : (
|
|
<>
|
|
{t("infrastructureServerRequirement")}{" "}
|
|
<Link
|
|
level="primary"
|
|
url="https://github.com/bluewave-labs/checkmate-agent"
|
|
label={t("common.monitoringAgentName")}
|
|
/>
|
|
</>
|
|
)}
|
|
</Typography>
|
|
</Stack>
|
|
<Stack gap={theme.spacing(8)}>
|
|
<TextInput
|
|
type="url"
|
|
id="url"
|
|
name="url"
|
|
startAdornment={<HttpAdornment https={https} />}
|
|
placeholder={
|
|
monitorType === "docker"
|
|
? "192.168.1.100:59232"
|
|
: "localhost:59232/api/v1/metrics"
|
|
}
|
|
label={
|
|
monitorType === "docker"
|
|
? "Capture Server URL"
|
|
: t("infrastructureServerUrlLabel")
|
|
}
|
|
https={https}
|
|
value={infrastructureMonitor.url}
|
|
onChange={onChange}
|
|
error={errors["url"] ? true : false}
|
|
helperText={errors["url"]}
|
|
disabled={!isCreate}
|
|
/>
|
|
{isCreate && (
|
|
<FieldWrapper
|
|
label={t("infrastructureProtocol")}
|
|
labelVariant="p"
|
|
>
|
|
<ButtonGroup>
|
|
<Button
|
|
variant="group"
|
|
filled={https.toString()}
|
|
onClick={() => setHttps(true)}
|
|
>
|
|
{t("https")}
|
|
</Button>
|
|
<Button
|
|
variant="group"
|
|
filled={(!https).toString()}
|
|
onClick={() => setHttps(false)}
|
|
>
|
|
{t("http")}
|
|
</Button>
|
|
</ButtonGroup>
|
|
</FieldWrapper>
|
|
)}
|
|
<TextInput
|
|
type="text"
|
|
id="name"
|
|
name="name"
|
|
label={t("infrastructureDisplayNameLabel")}
|
|
placeholder="Google"
|
|
isOptional={true}
|
|
value={infrastructureMonitor.name}
|
|
onChange={onChange}
|
|
error={errors["name"]}
|
|
/>
|
|
<TextInput
|
|
type="text"
|
|
id="secret"
|
|
name="secret"
|
|
label={
|
|
monitorType === "docker"
|
|
? "Capture API Secret"
|
|
: t("infrastructureAuthorizationSecretLabel")
|
|
}
|
|
placeholder={monitorType === "docker" ? "Bearer token for Capture API" : ""}
|
|
value={infrastructureMonitor.secret}
|
|
onChange={onChange}
|
|
error={errors["secret"] ? true : false}
|
|
helperText={errors["secret"]}
|
|
/>
|
|
</Stack>
|
|
</ConfigBox>
|
|
<ConfigBox>
|
|
<Box>
|
|
<Typography
|
|
component="h2"
|
|
variant="h2"
|
|
>
|
|
{t("notificationConfig.title")}
|
|
</Typography>
|
|
<Typography component="p">{t("notificationConfig.description")}</Typography>
|
|
</Box>
|
|
<NotificationsConfig
|
|
notifications={notifications}
|
|
setMonitor={setInfrastructureMonitor}
|
|
setNotifications={infrastructureMonitor.notifications}
|
|
/>
|
|
</ConfigBox>
|
|
<ConfigBox>
|
|
<Box>
|
|
<Typography
|
|
component="h2"
|
|
variant="h2"
|
|
>
|
|
{t("createMonitorPage.incidentConfigTitle")}
|
|
</Typography>
|
|
<Typography component="p">
|
|
{t("createMonitorPage.incidentConfigDescription")}
|
|
</Typography>
|
|
</Box>
|
|
<Stack gap={theme.spacing(20)}>
|
|
<TextInput
|
|
name="statusWindowSize"
|
|
label={t("createMonitorPage.incidentConfigStatusWindowLabel")}
|
|
type="number"
|
|
value={infrastructureMonitor.statusWindowSize}
|
|
onChange={onChange}
|
|
error={errors["statusWindowSize"] ? true : false}
|
|
helperText={errors["statusWindowSize"]}
|
|
/>
|
|
<TextInput
|
|
name="statusWindowThreshold"
|
|
label={t("createMonitorPage.incidentConfigStatusWindowThresholdLabel")}
|
|
type="number"
|
|
value={infrastructureMonitor.statusWindowThreshold}
|
|
onChange={onChange}
|
|
error={errors["statusWindowThreshold"] ? true : false}
|
|
helperText={errors["statusWindowThreshold"]}
|
|
/>
|
|
</Stack>
|
|
</ConfigBox>
|
|
{monitorType === "hardware" && (
|
|
<CustomAlertsSection
|
|
errors={errors}
|
|
onChange={onChange}
|
|
infrastructureMonitor={infrastructureMonitor}
|
|
handleCheckboxChange={handleCheckboxChange}
|
|
/>
|
|
)}
|
|
<ConfigBox>
|
|
<Box>
|
|
<Typography
|
|
component="h2"
|
|
variant="h2"
|
|
>
|
|
{t("distributedUptimeCreateAdvancedSettings")}
|
|
</Typography>
|
|
</Box>
|
|
<Stack gap={theme.spacing(12)}>
|
|
<Select
|
|
id="interval"
|
|
name="interval"
|
|
label="Check frequency"
|
|
value={infrastructureMonitor.interval || 15}
|
|
onChange={onChange}
|
|
items={FREQUENCIES}
|
|
/>
|
|
</Stack>
|
|
</ConfigBox>
|
|
<Stack
|
|
direction="row"
|
|
justifyContent="flex-end"
|
|
>
|
|
<Button
|
|
type="submit"
|
|
variant="contained"
|
|
color="accent"
|
|
loading={isBusy}
|
|
>
|
|
{t(isCreate ? "infrastructureCreateMonitor" : "infrastructureEditMonitor")}
|
|
</Button>
|
|
</Stack>
|
|
</Stack>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default CreateInfrastructureMonitor;
|