infra thresholds FE
This commit is contained in:
1 parent
7631e65cd2
commit
0453b790af
9 files changed
+191
-25
No files matched your search
@@ -17,7 +17,8 @@ export const useInitForm = ({
|
||||
}
|
||||
}
|
||||
|
||||
const defaults: z.infer<typeof monitorSchemaInfra> = {
|
||||
type InfraValues = z.infer<typeof monitorSchemaInfra>;
|
||||
const defaults: InfraValues = {
|
||||
type: "infrastructure",
|
||||
url: initialData?.url || "",
|
||||
secret: initialData?.secret || "",
|
||||
@@ -26,6 +27,12 @@ export const useInitForm = ({
|
||||
name: initialData?.name || "",
|
||||
interval: humanInterval,
|
||||
rejectUnauthorized: true,
|
||||
thresholds: initialData?.thresholds ?? {
|
||||
cpu: 100,
|
||||
memory: 100,
|
||||
disk: 100,
|
||||
temperature: 100,
|
||||
},
|
||||
};
|
||||
return { defaults };
|
||||
}, [initialData]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BasePage, InfoBox, PlanCard } from "@/components/design-elements";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { Button } from "@/components/inputs";
|
||||
|
||||
import { useAppDispatch, useAppSelector } from "@/hooks/AppHooks";
|
||||
@@ -27,7 +26,6 @@ const BillingPage = () => {
|
||||
const { post, loading: isPosting } = usePost();
|
||||
const plans = response?.data ?? [];
|
||||
|
||||
const theme = useTheme();
|
||||
const selectedPlan = user?.entitlements?.plan || null;
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedTeamId = useAppSelector((s) => s.auth.selectedTeamId);
|
||||
|
||||
@@ -37,8 +37,12 @@ const InfraConfigurePage = () => {
|
||||
secret: data.secret,
|
||||
name: data.name,
|
||||
notificationChannels: data.notificationChannels,
|
||||
thresholds: data.thresholds,
|
||||
interval,
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(submitData, null, 2));
|
||||
|
||||
const result = await patch(`/monitors/${id}`, submitData);
|
||||
if (result) {
|
||||
navigate(`/infrastructure/${id}`);
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { InfraForm } from "@/pages/infrastructure/InfraForm";
|
||||
|
||||
import humanInterval from "human-interval";
|
||||
import { useNavigate } from "react-router";
|
||||
import { z } from "zod";
|
||||
import { monitorSchemaInfra } from "@/validation/zod";
|
||||
import { useGet, usePost } from "@/hooks/UseApi";
|
||||
import type { ApiResponse } from "@/hooks/UseApi";
|
||||
import { useNavigate } from "react-router";
|
||||
import type { INotificationChannel } from "@/types/notification-channel";
|
||||
const InfraCreatePage = () => {
|
||||
type FormValues = z.infer<typeof monitorSchemaInfra>;
|
||||
type SubmitValues = Omit<FormValues, "interval"> & {
|
||||
interval: number | undefined;
|
||||
};
|
||||
const navigate = useNavigate();
|
||||
const { response } = useGet<ApiResponse<INotificationChannel[]>>(
|
||||
"/notification-channels"
|
||||
);
|
||||
const { post, loading, error } = usePost<SubmitValues, any>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
let interval = humanInterval(data.interval);
|
||||
|
||||
@@ -190,7 +190,9 @@ export const InfraForm = ({
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
fieldLabel={t("createMonitorPage.incidentConfigStatusCheckNumber")}
|
||||
fieldLabel={t(
|
||||
"createMonitorPage.incidentConfigStatusCheckNumber"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!errors.n}
|
||||
helperText={errors.n ? errors.n.message : ""}
|
||||
@@ -203,6 +205,70 @@ export const InfraForm = ({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("Threshold Alerts")}
|
||||
subtitle={t("Set percentage thresholds to trigger incidents.")}
|
||||
rightContent={
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Controller
|
||||
name="thresholds.cpu"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
fieldLabel={t("CPU %")}
|
||||
fullWidth
|
||||
error={!!errors.thresholds?.cpu}
|
||||
helperText={errors.thresholds?.cpu?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="thresholds.memory"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
fieldLabel={t("Memory %")}
|
||||
fullWidth
|
||||
error={!!errors.thresholds?.memory}
|
||||
helperText={errors.thresholds?.memory?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="thresholds.disk"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
fieldLabel={t("Disk %")}
|
||||
fullWidth
|
||||
error={!!errors.thresholds?.disk}
|
||||
helperText={errors.thresholds?.disk?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="thresholds.temperature"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
fieldLabel={t("Temperature °C")}
|
||||
fullWidth
|
||||
error={!!errors.thresholds?.temperature}
|
||||
helperText={errors.thresholds?.temperature?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("notificationConfig.title")}
|
||||
subtitle={t("notificationConfig.description")}
|
||||
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
declare module "*.svg?react" {
|
||||
import * as React from "react";
|
||||
const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
export default ReactComponent;
|
||||
}
|
||||
|
||||
declare module "*.svg" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
@@ -158,6 +158,65 @@ export const monitorSchemaInfra = z.object({
|
||||
name: z.string().min(1, "Display name is required"),
|
||||
interval: durationSchemaInfra,
|
||||
rejectUnauthorized: z.boolean().default(true),
|
||||
thresholds: z.object({
|
||||
// Accept both strings and numbers from the form. Enforce required and ranges with clear messages.
|
||||
cpu: z.preprocess(
|
||||
(val) => (typeof val === "number" ? String(val) : val),
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "CPU threshold is required" })
|
||||
.refine((v) => /^-?\d+(\.\d+)?$/.test(v), {
|
||||
message: "CPU must be a number",
|
||||
})
|
||||
.transform((v) => Number(v))
|
||||
.refine((v) => v >= 0 && v <= 100, {
|
||||
message: "CPU must be between 0 and 100",
|
||||
})
|
||||
),
|
||||
memory: z.preprocess(
|
||||
(val) => (typeof val === "number" ? String(val) : val),
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Memory threshold is required" })
|
||||
.refine((v) => /^-?\d+(\.\d+)?$/.test(v), {
|
||||
message: "Memory must be a number",
|
||||
})
|
||||
.transform((v) => Number(v))
|
||||
.refine((v) => v >= 0 && v <= 100, {
|
||||
message: "Memory must be between 0 and 100",
|
||||
})
|
||||
),
|
||||
disk: z.preprocess(
|
||||
(val) => (typeof val === "number" ? String(val) : val),
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Disk threshold is required" })
|
||||
.refine((v) => /^-?\d+(\.\d+)?$/.test(v), {
|
||||
message: "Disk must be a number",
|
||||
})
|
||||
.transform((v) => Number(v))
|
||||
.refine((v) => v >= 0 && v <= 100, {
|
||||
message: "Disk must be between 0 and 100",
|
||||
})
|
||||
),
|
||||
temperature: z.preprocess(
|
||||
(val) => (typeof val === "number" ? String(val) : val),
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Temperature threshold is required" })
|
||||
.refine((v) => /^-?\d+(\.\d+)?$/.test(v), {
|
||||
message: "Temperature must be a number",
|
||||
})
|
||||
.transform((v) => Number(v))
|
||||
.refine((v) => v >= -50 && v <= 150, {
|
||||
message: "Temperature must be between -50 and 150",
|
||||
})
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export const teamSchema = z.object({
|
||||
|
||||
@@ -191,12 +191,17 @@ class StatusService implements IStatusService {
|
||||
const breaches: string[] = [];
|
||||
const metrics: ThresholdEvaluationResult["metrics"] = emptyMetrics();
|
||||
|
||||
const cpu = check.system.cpu?.usage_percent;
|
||||
// usage values in check.system are 0..1 decimals; thresholds are 0..100
|
||||
const cpuRaw = check.system.cpu?.usage_percent;
|
||||
const cpuPct = typeof cpuRaw === "number" ? cpuRaw * 100 : undefined;
|
||||
const cpuThreshold = typeof t.cpu === "number" ? t.cpu : undefined;
|
||||
metrics.cpu = {
|
||||
value: typeof cpu === "number" ? cpu : undefined,
|
||||
threshold: typeof t.cpu === "number" ? t.cpu : undefined,
|
||||
value: cpuPct,
|
||||
threshold: cpuThreshold,
|
||||
breached:
|
||||
typeof t.cpu === "number" && typeof cpu === "number" && cpu > t.cpu,
|
||||
cpuPct !== undefined &&
|
||||
cpuThreshold !== undefined &&
|
||||
cpuPct > cpuThreshold,
|
||||
};
|
||||
if (
|
||||
metrics.cpu.breached &&
|
||||
@@ -206,14 +211,16 @@ class StatusService implements IStatusService {
|
||||
breaches.push(`cpu ${metrics.cpu.value}% > ${metrics.cpu.threshold}%`);
|
||||
}
|
||||
|
||||
const mem = check.system.memory?.usage_percent;
|
||||
const memRaw = check.system.memory?.usage_percent;
|
||||
const memPct = typeof memRaw === "number" ? memRaw * 100 : undefined;
|
||||
const memThreshold = typeof t.memory === "number" ? t.memory : undefined;
|
||||
metrics.memory = {
|
||||
value: typeof mem === "number" ? mem : undefined,
|
||||
threshold: typeof t.memory === "number" ? t.memory : undefined,
|
||||
value: memPct,
|
||||
threshold: memThreshold,
|
||||
breached:
|
||||
typeof t.memory === "number" &&
|
||||
typeof mem === "number" &&
|
||||
mem > t.memory,
|
||||
memPct !== undefined &&
|
||||
memThreshold !== undefined &&
|
||||
memPct > memThreshold,
|
||||
};
|
||||
if (
|
||||
metrics.memory.breached &&
|
||||
@@ -225,18 +232,32 @@ class StatusService implements IStatusService {
|
||||
);
|
||||
}
|
||||
|
||||
const diskUsages: number[] = Array.isArray(check.system.disk)
|
||||
const diskUsagesRaw: number[] = Array.isArray(check.system.disk)
|
||||
? check.system.disk
|
||||
.map((d) => d?.usage_percent)
|
||||
.filter((n): n is number => typeof n === "number")
|
||||
: [];
|
||||
const maxDisk = diskUsages.length ? Math.max(...diskUsages) : undefined;
|
||||
const diskUsagesPct = diskUsagesRaw.map((n) => n * 100);
|
||||
const maxDiskPct = diskUsagesPct.length
|
||||
? Math.max(...diskUsagesPct)
|
||||
: undefined;
|
||||
const diskThreshold = typeof t.disk === "number" ? t.disk : undefined;
|
||||
metrics.disk = {
|
||||
value: maxDiskPct,
|
||||
threshold: diskThreshold,
|
||||
breached:
|
||||
maxDiskPct !== undefined &&
|
||||
diskThreshold !== undefined &&
|
||||
maxDiskPct > diskThreshold,
|
||||
};
|
||||
if (
|
||||
typeof t.disk === "number" &&
|
||||
typeof maxDisk === "number" &&
|
||||
maxDisk > t.disk
|
||||
metrics.disk.breached &&
|
||||
metrics.disk.value !== undefined &&
|
||||
metrics.disk.threshold !== undefined
|
||||
) {
|
||||
breaches.push(`disk ${maxDisk}% > ${t.disk}%`);
|
||||
breaches.push(
|
||||
`disk ${metrics.disk.value}% > ${metrics.disk.threshold}%`
|
||||
);
|
||||
}
|
||||
|
||||
const temps: number[] = Array.isArray(check.system.cpu?.temperature)
|
||||
@@ -244,6 +265,7 @@ class StatusService implements IStatusService {
|
||||
(n): n is number => typeof n === "number"
|
||||
)
|
||||
: [];
|
||||
|
||||
const maxTemp = temps.length ? Math.max(...temps) : undefined;
|
||||
if (
|
||||
typeof t.temperature === "number" &&
|
||||
@@ -254,9 +276,9 @@ class StatusService implements IStatusService {
|
||||
}
|
||||
|
||||
const hasBreach = breaches.length > 0;
|
||||
|
||||
return { hasBreach, notes: breaches, metrics };
|
||||
} catch {
|
||||
// best-effort; do not throw from evaluator
|
||||
return { hasBreach: false, notes: [], metrics: emptyMetrics() };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ export const monitorSchema = z
|
||||
cpu: z.number().min(0).max(100).optional(),
|
||||
memory: z.number().min(0).max(100).optional(),
|
||||
disk: z.number().min(0).max(100).optional(),
|
||||
temperature: z.number().min(0).max(150).optional(),
|
||||
temperature: z.number().min(-50).max(150).optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user