infra refactor
This commit is contained in:
1 parent
df707ab194
commit
4dfeb71a9d
14 files changed
+195
-187
No files matched your search
@@ -1,39 +1,66 @@
|
||||
import { monitorSchemaInfra } from "@/validation/zod";
|
||||
import ms from "ms";
|
||||
import { useMemo } from "react";
|
||||
import type {
|
||||
FormValues,
|
||||
SubmitValues,
|
||||
} from "@/pages/infrastructure/InfraForm";
|
||||
import type { IMonitor } from "@/types/monitor";
|
||||
import humanInterval from "human-interval";
|
||||
|
||||
import { z } from "zod";
|
||||
export const useInitForm = ({
|
||||
initialData,
|
||||
}: {
|
||||
initialData: Partial<z.infer<typeof monitorSchemaInfra>> | undefined;
|
||||
initialData: IMonitor | undefined;
|
||||
}) => {
|
||||
return useMemo(() => {
|
||||
let humanInterval = "1 minute";
|
||||
if (initialData?.interval) {
|
||||
const parsed = Number(initialData.interval);
|
||||
if (!isNaN(parsed)) {
|
||||
humanInterval = ms(parsed, { long: true });
|
||||
}
|
||||
}
|
||||
|
||||
type InfraValues = z.infer<typeof monitorSchemaInfra>;
|
||||
const defaults: InfraValues = {
|
||||
type: "infrastructure",
|
||||
url: initialData?.url || "",
|
||||
secret: initialData?.secret || "",
|
||||
n: initialData?.n || 3,
|
||||
notificationChannels: initialData?.notificationChannels || [],
|
||||
name: initialData?.name || "",
|
||||
interval: humanInterval,
|
||||
rejectUnauthorized: true,
|
||||
thresholds: initialData?.thresholds ?? {
|
||||
cpu: 100,
|
||||
memory: 100,
|
||||
disk: 100,
|
||||
temperature: 100,
|
||||
},
|
||||
const apiToForm = (apiData: IMonitor): FormValues => {
|
||||
const formData: FormValues = {
|
||||
type: apiData.type,
|
||||
url: apiData.url,
|
||||
secret: apiData.secret,
|
||||
n: apiData.n,
|
||||
notificationChannels: apiData.notificationChannels,
|
||||
name: apiData.name,
|
||||
interval: ms(apiData.interval, { long: true }),
|
||||
rejectUnauthorized: apiData.rejectUnauthorized,
|
||||
thresholds: apiData.thresholds,
|
||||
};
|
||||
return formData;
|
||||
};
|
||||
return { defaults };
|
||||
|
||||
const formToApi = (formData: FormValues): SubmitValues => {
|
||||
const submitData: SubmitValues = {
|
||||
type: formData.type,
|
||||
url: formData.url,
|
||||
secret: formData.secret,
|
||||
n: formData.n,
|
||||
notificationChannels: formData.notificationChannels,
|
||||
name: formData.name,
|
||||
interval: humanInterval(formData.interval),
|
||||
rejectUnauthorized: formData.rejectUnauthorized,
|
||||
thresholds: formData.thresholds,
|
||||
};
|
||||
return submitData;
|
||||
};
|
||||
|
||||
const defaults: FormValues = initialData
|
||||
? apiToForm(initialData)
|
||||
: {
|
||||
type: "infrastructure",
|
||||
url: "",
|
||||
secret: "",
|
||||
n: 3,
|
||||
notificationChannels: [],
|
||||
name: "",
|
||||
interval: "1 minute",
|
||||
rejectUnauthorized: true,
|
||||
thresholds: {
|
||||
cpu: 100,
|
||||
memory: 100,
|
||||
disk: 100,
|
||||
temperature: 100,
|
||||
},
|
||||
};
|
||||
return { defaults, apiToForm, formToApi };
|
||||
}, [initialData]);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { useMemo } from "react";
|
||||
import humanInterval from "human-interval";
|
||||
import type { FormValues, SubmitValues } from "@/pages/uptime/UptimeForm";
|
||||
import type { IMonitor } from "@/types/monitor";
|
||||
|
||||
import ms from "ms";
|
||||
|
||||
export const useInitForm = ({
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { monitorSchemaInfra } from "@/validation/zod";
|
||||
import { monitorSchemaInfra } from "@/validation";
|
||||
import { useGet, usePatch } from "@/hooks/UseApi";
|
||||
import { InfraForm } from "@/pages/infrastructure/InfraForm";
|
||||
|
||||
import { useParams } from "react-router";
|
||||
import type { ApiResponse } from "@/types/api";
|
||||
import humanInterval from "human-interval";
|
||||
import { z } from "zod";
|
||||
import { useNavigate } from "react-router";
|
||||
import type { INotificationChannel } from "@/types/notification-channel";
|
||||
import type { IMonitor } from "@/types/monitor";
|
||||
import type { SubmitValues } from "@/pages/infrastructure/InfraForm";
|
||||
|
||||
const InfraConfigurePage = () => {
|
||||
type FormValues = z.infer<typeof monitorSchemaInfra>;
|
||||
type SubmitValues = Omit<FormValues, "interval"> & {
|
||||
interval: number | undefined;
|
||||
};
|
||||
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { response } = useGet<ApiResponse<INotificationChannel[]>>(
|
||||
@@ -29,20 +23,12 @@ const InfraConfigurePage = () => {
|
||||
|
||||
const { patch, loading, error } = usePatch<Partial<SubmitValues>, IMonitor>();
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
let interval = humanInterval(data.interval);
|
||||
if (!interval) interval = 180000;
|
||||
const submitData = {
|
||||
type: data.type,
|
||||
secret: data.secret,
|
||||
name: data.name,
|
||||
notificationChannels: data.notificationChannels,
|
||||
thresholds: data.thresholds,
|
||||
interval,
|
||||
};
|
||||
if (!monitor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const result = await patch(`/monitors/${id}`, submitData);
|
||||
const onSubmit = async (data: SubmitValues) => {
|
||||
const result = await patch(`/monitors/${id}`, data);
|
||||
if (result) {
|
||||
navigate(`/infrastructure/${id}`);
|
||||
} else {
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
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 "@/types/api";
|
||||
import type { INotificationChannel } from "@/types/notification-channel";
|
||||
import type { SubmitValues } from "@/pages/infrastructure/InfraForm";
|
||||
|
||||
const InfraCreatePage = () => {
|
||||
type FormValues = z.infer<typeof monitorSchemaInfra>;
|
||||
type SubmitValues = Omit<FormValues, "interval"> & {
|
||||
interval: number | undefined;
|
||||
};
|
||||
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);
|
||||
if (!interval) interval = 60000;
|
||||
const submitData = { ...data, interval };
|
||||
const result = await post("/monitors", submitData);
|
||||
const onSubmit = async (data: SubmitValues) => {
|
||||
const result = await post("/monitors", data);
|
||||
if (result) {
|
||||
navigate("/infrastructure");
|
||||
} else {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Typography } from "@mui/material";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { monitorSchemaInfra } from "@/validation/zod";
|
||||
import { monitorSchemaInfra } from "@/validation";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
@@ -24,8 +24,12 @@ import {
|
||||
} from "react-hook-form";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useInitForm } from "@/hooks/forms/UseInitInfraForm";
|
||||
import type { IMonitor } from "@/types/monitor";
|
||||
|
||||
type FormValues = z.infer<typeof monitorSchemaInfra>;
|
||||
export type FormValues = z.infer<typeof monitorSchemaInfra>;
|
||||
export type SubmitValues = Omit<FormValues, "interval"> & {
|
||||
interval: number | undefined;
|
||||
};
|
||||
|
||||
export const InfraForm = ({
|
||||
mode = "create",
|
||||
@@ -35,14 +39,14 @@ export const InfraForm = ({
|
||||
loading,
|
||||
}: {
|
||||
mode?: "create" | "configure";
|
||||
initialData?: Partial<FormValues>;
|
||||
onSubmit: SubmitHandler<FormValues>;
|
||||
initialData?: IMonitor;
|
||||
onSubmit: SubmitHandler<SubmitValues>;
|
||||
notificationOptions: any[];
|
||||
loading: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { defaults } = useInitForm({ initialData: initialData });
|
||||
const { defaults, formToApi } = useInitForm({ initialData: initialData });
|
||||
const urlInputRef = useRef<HTMLInputElement>(null);
|
||||
const [selectedProtocol, setSelectedProtocol] = useState<string>("https");
|
||||
|
||||
@@ -97,8 +101,12 @@ export const InfraForm = ({
|
||||
name: "notificationChannels",
|
||||
});
|
||||
|
||||
const submitForm = (formData: FormValues) => {
|
||||
onSubmit(formToApi(formData));
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePage component={"form"} onSubmit={handleSubmit(onSubmit)}>
|
||||
<BasePage component={"form"} onSubmit={handleSubmit(submitForm)}>
|
||||
<ConfigBox
|
||||
title={t("monitors.common.form.general.title")}
|
||||
subtitle={t(`monitors.infrastructure.form.general.description`)}
|
||||
@@ -303,7 +311,7 @@ export const InfraForm = ({
|
||||
)}
|
||||
/>
|
||||
<Stack gap={theme.spacing(2)} mt={theme.spacing(2)}>
|
||||
{notificationChannels.map((notificationId) => {
|
||||
{notificationChannels.map((notificationId: string) => {
|
||||
const option = notificationOptions.find(
|
||||
(o: any) => o._id === notificationId
|
||||
);
|
||||
@@ -321,7 +329,7 @@ export const InfraForm = ({
|
||||
strokeWidth={1.5}
|
||||
onClick={() => {
|
||||
const updated = notificationChannels.filter(
|
||||
(id) => id !== notificationId
|
||||
(id: string) => id !== notificationId
|
||||
);
|
||||
setValue("notificationChannels", updated);
|
||||
}}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useLimitReached } from "@/hooks/UsePlanEntitlements";
|
||||
|
||||
const GLOBAL_REFRESH = config.GLOBAL_REFRESH;
|
||||
|
||||
const PageSpeedMonitorsPage = () => {
|
||||
const InfraMonitorsPage = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
@@ -131,4 +131,4 @@ const PageSpeedMonitorsPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSpeedMonitorsPage;
|
||||
export default InfraMonitorsPage;
|
||||
@@ -104,7 +104,6 @@ export const UptimeForm = ({
|
||||
} else {
|
||||
setValue("url", `${selectedType}://${url}`);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedType, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -39,6 +39,13 @@ export interface IMonitor {
|
||||
updatedAt: string;
|
||||
updatedBy: string;
|
||||
url: string;
|
||||
secret: string;
|
||||
thresholds: {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
disk: number;
|
||||
temperature: number;
|
||||
};
|
||||
__v: number;
|
||||
_id: string;
|
||||
}
|
||||
|
||||
@@ -3,3 +3,5 @@ export * from "./login";
|
||||
export * from "./register";
|
||||
export * from "./monitor";
|
||||
export * from "./pagespeed";
|
||||
export * from "./infra";
|
||||
export * from "./utils";
|
||||
@@ -0,0 +1,74 @@
|
||||
import { z } from "zod";
|
||||
import { urlRegex, durationSchema } from "@/validation/utils";
|
||||
|
||||
export const monitorSchemaInfra = z.object({
|
||||
type: z.string().min(1, "You must select an option"),
|
||||
url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"),
|
||||
secret: z.string().min(1, "Secret is required"),
|
||||
n: z.coerce
|
||||
.number({ message: "Number required" })
|
||||
.min(1, "Minimum value is 1")
|
||||
.max(25, "Maximum value is 25"),
|
||||
notificationChannels: z.array(z.string()).optional().default([]),
|
||||
name: z.string().min(1, "Display name is required"),
|
||||
interval: durationSchema(60000),
|
||||
rejectUnauthorized: z.boolean().default(true),
|
||||
thresholds: z.object({
|
||||
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",
|
||||
})
|
||||
),
|
||||
}),
|
||||
});
|
||||
@@ -1,25 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import humanInterval from "human-interval";
|
||||
import ms from "ms";
|
||||
export const urlRegex =
|
||||
/^(https?:\/\/)?(localhost|([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+|(\d{1,3}\.){3}\d{1,3})(:\d{1,5})?(\/.*)?$/;
|
||||
|
||||
export const durationSchema = (minInterval: number) =>
|
||||
z.string().superRefine((val, ctx) => {
|
||||
const milis = humanInterval(val);
|
||||
|
||||
if (!milis || isNaN(milis)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Invalid duration format",
|
||||
});
|
||||
} else if (milis < minInterval) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: `Minimum duration is ${ms(minInterval, { long: true })}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
import { urlRegex, durationSchema } from "@/validation/utils";
|
||||
|
||||
export const monitorSchema = z
|
||||
.object({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { urlRegex, durationSchema } from "@/validation";
|
||||
import { urlRegex, durationSchema } from "@/validation/utils";
|
||||
|
||||
export const monitorSchemaPageSpeed = z.object({
|
||||
type: z.string().min(1, "You must select an option"),
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { z } from "zod";
|
||||
import humanInterval from "human-interval";
|
||||
import ms from "ms";
|
||||
|
||||
export const urlRegex =
|
||||
/^(https?:\/\/)?(localhost|([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+|(\d{1,3}\.){3}\d{1,3})(:\d{1,5})?(\/.*)?$/;
|
||||
|
||||
export const durationSchema = (minInterval: number) =>
|
||||
z.string().superRefine((val, ctx) => {
|
||||
const milis = humanInterval(val);
|
||||
|
||||
if (!milis || isNaN(milis)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Invalid duration format",
|
||||
});
|
||||
} else if (milis < minInterval) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: `Minimum duration is ${ms(minInterval, { long: true })}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import humanInterval from "human-interval";
|
||||
// humanInterval moved to split schema files
|
||||
import { ChannelTypes } from "@/types/notification-channel";
|
||||
import { MaintenanceRepeats } from "@/types/maintenance";
|
||||
|
||||
@@ -8,25 +8,7 @@ const urlRegex =
|
||||
|
||||
// monitor duration schema moved to client/src/validation/monitor.ts
|
||||
// pagespeed duration schema moved to client/src/validation/pagespeed.ts
|
||||
const durationSchemaInfra = z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val || val.trim() === "") return;
|
||||
const ms = humanInterval(val);
|
||||
|
||||
if (!ms || isNaN(ms)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Invalid duration format",
|
||||
});
|
||||
} else if (ms < 60000) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Minimum duration is 1 minute",
|
||||
});
|
||||
}
|
||||
});
|
||||
// infra duration schema moved to client/src/validation/infra.ts
|
||||
|
||||
const optionalString = <T extends z.ZodTypeAny>(schema: T) =>
|
||||
z.preprocess(
|
||||
@@ -38,78 +20,7 @@ const optionalString = <T extends z.ZodTypeAny>(schema: T) =>
|
||||
// moved to client/src/validation/monitor.ts
|
||||
|
||||
// moved to client/src/validation/pagespeed.ts
|
||||
export const monitorSchemaInfra = z.object({
|
||||
type: z.string().min(1, "You must select an option"),
|
||||
url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"),
|
||||
secret: z.string().min(1, "Secret is required"),
|
||||
n: z.coerce
|
||||
.number({ message: "Number required" })
|
||||
.min(1, "Minimum value is 1")
|
||||
.max(25, "Maximum value is 25"),
|
||||
notificationChannels: z.array(z.string()).optional().default([]),
|
||||
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",
|
||||
})
|
||||
),
|
||||
}),
|
||||
});
|
||||
// moved to client/src/validation/infra.ts
|
||||
|
||||
export const teamSchema = z.object({
|
||||
name: z.string().min(1, "Team name is required"),
|
||||
|
||||
Reference in new issue
Block a user