mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-04 11:34:19 +00:00
Standardize the healch check form between the two
This commit is contained in:
417
src/components/HealthCheckCredenza.tsx
Normal file
417
src/components/HealthCheckCredenza.tsx
Normal file
@@ -0,0 +1,417 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { HealthCheckFormFields } from "@app/components/HealthCheckFormFields";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@/components/Credenza";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export type HealthCheckConfig = {
|
||||
hcEnabled: boolean;
|
||||
hcPath: string;
|
||||
hcMethod: string;
|
||||
hcInterval: number;
|
||||
hcTimeout: number;
|
||||
hcStatus: number | null;
|
||||
hcHeaders?: { name: string; value: string }[] | null;
|
||||
hcScheme?: string;
|
||||
hcHostname: string;
|
||||
hcPort: number;
|
||||
hcFollowRedirects: boolean;
|
||||
hcMode: string;
|
||||
hcUnhealthyInterval: number;
|
||||
hcTlsServerName: string;
|
||||
hcHealthyThreshold: number;
|
||||
hcUnhealthyThreshold: number;
|
||||
};
|
||||
|
||||
export type HealthCheckRow = {
|
||||
targetHealthCheckId: number;
|
||||
name: string;
|
||||
hcEnabled: boolean;
|
||||
hcHealth: "unknown" | "healthy" | "unhealthy";
|
||||
hcMode: string | null;
|
||||
hcHostname: string | null;
|
||||
hcPort: number | null;
|
||||
hcPath: string | null;
|
||||
hcScheme: string | null;
|
||||
hcMethod: string | null;
|
||||
hcInterval: number | null;
|
||||
hcUnhealthyInterval: number | null;
|
||||
hcTimeout: number | null;
|
||||
hcHeaders: string | null;
|
||||
hcFollowRedirects: boolean | null;
|
||||
hcStatus: number | null;
|
||||
hcTlsServerName: string | null;
|
||||
hcHealthyThreshold: number | null;
|
||||
hcUnhealthyThreshold: number | null;
|
||||
};
|
||||
|
||||
export type HealthCheckCredenzaProps =
|
||||
| {
|
||||
mode: "autoSave";
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
orgId?: string;
|
||||
targetAddress: string;
|
||||
targetMethod?: string;
|
||||
initialConfig?: Partial<HealthCheckConfig>;
|
||||
onChanges: (config: HealthCheckConfig) => Promise<void>;
|
||||
}
|
||||
| {
|
||||
mode: "submit";
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
orgId: string;
|
||||
initialValues?: HealthCheckRow | null;
|
||||
onSaved: () => void;
|
||||
};
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
name: "",
|
||||
hcEnabled: true,
|
||||
hcMode: "http",
|
||||
hcScheme: "https",
|
||||
hcMethod: "GET",
|
||||
hcHostname: "",
|
||||
hcPort: "",
|
||||
hcPath: "/",
|
||||
hcInterval: 30,
|
||||
hcUnhealthyInterval: 30,
|
||||
hcTimeout: 5,
|
||||
hcHealthyThreshold: 1,
|
||||
hcUnhealthyThreshold: 1,
|
||||
hcFollowRedirects: true,
|
||||
hcTlsServerName: "",
|
||||
hcStatus: null as number | null,
|
||||
hcHeaders: [] as { name: string; value: string }[]
|
||||
};
|
||||
|
||||
export function HealthCheckCredenza(props: HealthCheckCredenzaProps) {
|
||||
const { mode, open, setOpen, orgId } = props;
|
||||
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const healthCheckSchema = z
|
||||
.object({
|
||||
...(mode === "submit"
|
||||
? {
|
||||
name: z
|
||||
.string()
|
||||
.min(1, { message: t("standaloneHcNameLabel") })
|
||||
}
|
||||
: {}),
|
||||
hcEnabled: z.boolean(),
|
||||
hcPath: z.string().optional(),
|
||||
hcMethod: z.string().optional(),
|
||||
hcInterval: z
|
||||
.int()
|
||||
.positive()
|
||||
.min(5, { message: t("healthCheckIntervalMin") }),
|
||||
hcTimeout: z
|
||||
.int()
|
||||
.positive()
|
||||
.min(1, { message: t("healthCheckTimeoutMin") }),
|
||||
hcStatus: z.int().positive().min(100).optional().nullable(),
|
||||
hcHeaders: z
|
||||
.array(z.object({ name: z.string(), value: z.string() }))
|
||||
.nullable()
|
||||
.optional(),
|
||||
hcScheme: z.string().optional(),
|
||||
hcHostname: z.string(),
|
||||
hcPort: z
|
||||
.string()
|
||||
.min(1, { message: t("healthCheckPortInvalid") })
|
||||
.refine(
|
||||
(val) => {
|
||||
const port = parseInt(val);
|
||||
return port > 0 && port <= 65535;
|
||||
},
|
||||
{ message: t("healthCheckPortInvalid") }
|
||||
),
|
||||
hcFollowRedirects: z.boolean(),
|
||||
hcMode: z.string(),
|
||||
hcUnhealthyInterval: z.int().positive().min(5),
|
||||
hcTlsServerName: z.string(),
|
||||
hcHealthyThreshold: z
|
||||
.int()
|
||||
.positive()
|
||||
.min(1, { message: t("healthCheckHealthyThresholdMin") }),
|
||||
hcUnhealthyThreshold: z
|
||||
.int()
|
||||
.positive()
|
||||
.min(1, { message: t("healthCheckUnhealthyThresholdMin") })
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.hcMode !== "tcp") {
|
||||
if (!data.hcPath || data.hcPath.length < 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t("healthCheckPathRequired"),
|
||||
path: ["hcPath"]
|
||||
});
|
||||
}
|
||||
if (!data.hcMethod || data.hcMethod.length < 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t("healthCheckMethodRequired"),
|
||||
path: ["hcMethod"]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof healthCheckSchema>;
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(healthCheckSchema),
|
||||
defaultValues: mode === "submit" ? DEFAULT_VALUES : {}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
if (mode === "autoSave") {
|
||||
const { initialConfig, targetMethod } = props;
|
||||
|
||||
const getDefaultScheme = () => {
|
||||
if (initialConfig?.hcScheme) return initialConfig.hcScheme;
|
||||
if (targetMethod === "https") return "https";
|
||||
return "http";
|
||||
};
|
||||
|
||||
form.reset({
|
||||
hcEnabled: initialConfig?.hcEnabled,
|
||||
hcPath: initialConfig?.hcPath,
|
||||
hcMethod: initialConfig?.hcMethod,
|
||||
hcInterval: initialConfig?.hcInterval,
|
||||
hcTimeout: initialConfig?.hcTimeout,
|
||||
hcStatus: initialConfig?.hcStatus,
|
||||
hcHeaders: initialConfig?.hcHeaders,
|
||||
hcScheme: getDefaultScheme(),
|
||||
hcHostname: initialConfig?.hcHostname,
|
||||
hcPort: initialConfig?.hcPort
|
||||
? initialConfig.hcPort.toString()
|
||||
: "",
|
||||
hcFollowRedirects: initialConfig?.hcFollowRedirects,
|
||||
hcMode: initialConfig?.hcMode ?? "http",
|
||||
hcUnhealthyInterval: initialConfig?.hcUnhealthyInterval,
|
||||
hcTlsServerName: initialConfig?.hcTlsServerName ?? "",
|
||||
hcHealthyThreshold: initialConfig?.hcHealthyThreshold ?? 1,
|
||||
hcUnhealthyThreshold: initialConfig?.hcUnhealthyThreshold ?? 1
|
||||
});
|
||||
} else {
|
||||
const { initialValues } = props;
|
||||
|
||||
if (initialValues) {
|
||||
let parsedHeaders: { name: string; value: string }[] = [];
|
||||
if (initialValues.hcHeaders) {
|
||||
try {
|
||||
parsedHeaders = JSON.parse(initialValues.hcHeaders);
|
||||
} catch {
|
||||
parsedHeaders = [];
|
||||
}
|
||||
}
|
||||
|
||||
form.reset({
|
||||
name: initialValues.name,
|
||||
hcEnabled: initialValues.hcEnabled,
|
||||
hcMode: initialValues.hcMode ?? "http",
|
||||
hcScheme: initialValues.hcScheme ?? "https",
|
||||
hcMethod: initialValues.hcMethod ?? "GET",
|
||||
hcHostname: initialValues.hcHostname ?? "",
|
||||
hcPort: initialValues.hcPort
|
||||
? initialValues.hcPort.toString()
|
||||
: "",
|
||||
hcPath: initialValues.hcPath ?? "/",
|
||||
hcInterval: initialValues.hcInterval ?? 30,
|
||||
hcUnhealthyInterval:
|
||||
initialValues.hcUnhealthyInterval ?? 30,
|
||||
hcTimeout: initialValues.hcTimeout ?? 5,
|
||||
hcHealthyThreshold:
|
||||
initialValues.hcHealthyThreshold ?? 1,
|
||||
hcUnhealthyThreshold:
|
||||
initialValues.hcUnhealthyThreshold ?? 1,
|
||||
hcFollowRedirects:
|
||||
initialValues.hcFollowRedirects ?? true,
|
||||
hcTlsServerName: initialValues.hcTlsServerName ?? "",
|
||||
hcStatus: initialValues.hcStatus ?? null,
|
||||
hcHeaders: parsedHeaders
|
||||
});
|
||||
} else {
|
||||
form.reset(DEFAULT_VALUES);
|
||||
}
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleFieldChange = async (fieldName: string, value: any) => {
|
||||
if (mode !== "autoSave") return;
|
||||
try {
|
||||
const currentValues = form.getValues();
|
||||
const updatedValues = { ...currentValues, [fieldName]: value };
|
||||
|
||||
const configToSend: HealthCheckConfig = {
|
||||
...updatedValues,
|
||||
hcPath: updatedValues.hcPath ?? "",
|
||||
hcMethod: updatedValues.hcMethod ?? "",
|
||||
hcPort: parseInt(updatedValues.hcPort),
|
||||
hcStatus: updatedValues.hcStatus || null,
|
||||
hcHealthyThreshold: updatedValues.hcHealthyThreshold,
|
||||
hcUnhealthyThreshold: updatedValues.hcUnhealthyThreshold
|
||||
};
|
||||
|
||||
await props.onChanges(configToSend);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t("healthCheckError"),
|
||||
description: t("healthCheckErrorDescription"),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
if (mode !== "submit") return;
|
||||
const { initialValues, onSaved } = props;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload = {
|
||||
name: (values as any).name,
|
||||
hcEnabled: values.hcEnabled,
|
||||
hcMode: values.hcMode,
|
||||
hcScheme: values.hcScheme,
|
||||
hcMethod: values.hcMethod,
|
||||
hcHostname: values.hcHostname,
|
||||
hcPort: parseInt(values.hcPort),
|
||||
hcPath: values.hcPath ?? "",
|
||||
hcInterval: values.hcInterval,
|
||||
hcUnhealthyInterval: values.hcUnhealthyInterval,
|
||||
hcTimeout: values.hcTimeout,
|
||||
hcHealthyThreshold: values.hcHealthyThreshold,
|
||||
hcUnhealthyThreshold: values.hcUnhealthyThreshold,
|
||||
hcFollowRedirects: values.hcFollowRedirects,
|
||||
hcTlsServerName: values.hcTlsServerName,
|
||||
hcStatus: values.hcStatus || null,
|
||||
hcHeaders:
|
||||
values.hcHeaders && values.hcHeaders.length > 0
|
||||
? JSON.stringify(values.hcHeaders)
|
||||
: null
|
||||
};
|
||||
|
||||
if (initialValues) {
|
||||
await api.post(
|
||||
`/org/${orgId}/health-check/${initialValues.targetHealthCheckId}`,
|
||||
payload
|
||||
);
|
||||
} else {
|
||||
await api.put(`/org/${orgId}/health-check`, payload);
|
||||
}
|
||||
|
||||
toast({ title: t("standaloneHcSaved") });
|
||||
onSaved();
|
||||
setOpen(false);
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isEditing = mode === "submit" && !!(props as any).initialValues;
|
||||
|
||||
const title =
|
||||
mode === "autoSave"
|
||||
? t("configureHealthCheck")
|
||||
: isEditing
|
||||
? t("standaloneHcEditTitle")
|
||||
: t("standaloneHcCreateTitle");
|
||||
|
||||
const description =
|
||||
mode === "autoSave"
|
||||
? t("configureHealthCheckDescription", {
|
||||
target: (props as any).targetAddress
|
||||
})
|
||||
: t("standaloneHcDescription");
|
||||
|
||||
return (
|
||||
<Credenza open={open} onOpenChange={setOpen}>
|
||||
<CredenzaContent className="max-w-2xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{title}</CredenzaTitle>
|
||||
<CredenzaDescription>{description}</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hc-credenza-form"
|
||||
onSubmit={
|
||||
mode === "submit"
|
||||
? form.handleSubmit(onSubmit)
|
||||
: undefined
|
||||
}
|
||||
className="space-y-6"
|
||||
>
|
||||
<HealthCheckFormFields
|
||||
form={form}
|
||||
showNameField={mode === "submit"}
|
||||
hideEnabledField={mode === "submit"}
|
||||
onFieldChange={
|
||||
mode === "autoSave"
|
||||
? handleFieldChange
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
{mode === "autoSave" ? (
|
||||
<Button onClick={() => setOpen(false)}>
|
||||
{t("done")}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline" type="button">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
form="hc-credenza-form"
|
||||
disabled={loading}
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
|
||||
export default HealthCheckCredenza;
|
||||
@@ -1,223 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { HealthCheckFormFields } from "@app/components/HealthCheckFormFields";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@/components/Credenza";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type HealthCheckConfig = {
|
||||
hcEnabled: boolean;
|
||||
hcPath: string;
|
||||
hcMethod: string;
|
||||
hcInterval: number;
|
||||
hcTimeout: number;
|
||||
hcStatus: number | null;
|
||||
hcHeaders?: { name: string; value: string }[] | null;
|
||||
hcScheme?: string;
|
||||
hcHostname: string;
|
||||
hcPort: number;
|
||||
hcFollowRedirects: boolean;
|
||||
hcMode: string;
|
||||
hcUnhealthyInterval: number;
|
||||
hcTlsServerName: string;
|
||||
hcHealthyThreshold: number;
|
||||
hcUnhealthyThreshold: number;
|
||||
};
|
||||
|
||||
type HealthCheckDialogProps = {
|
||||
open: boolean;
|
||||
setOpen: (val: boolean) => void;
|
||||
orgId: string;
|
||||
targetAddress: string;
|
||||
targetMethod?: string;
|
||||
initialConfig?: Partial<HealthCheckConfig>;
|
||||
onChanges: (config: HealthCheckConfig) => Promise<void>;
|
||||
};
|
||||
|
||||
export default function HealthCheckDialog({
|
||||
open,
|
||||
setOpen,
|
||||
orgId,
|
||||
targetAddress,
|
||||
targetMethod,
|
||||
initialConfig,
|
||||
onChanges
|
||||
}: HealthCheckDialogProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const healthCheckSchema = z
|
||||
.object({
|
||||
hcEnabled: z.boolean(),
|
||||
hcPath: z.string().optional(),
|
||||
hcMethod: z.string().optional(),
|
||||
hcInterval: z
|
||||
.int()
|
||||
.positive()
|
||||
.min(5, { message: t("healthCheckIntervalMin") }),
|
||||
hcTimeout: z
|
||||
.int()
|
||||
.positive()
|
||||
.min(1, { message: t("healthCheckTimeoutMin") }),
|
||||
hcStatus: z.int().positive().min(100).optional().nullable(),
|
||||
hcHeaders: z
|
||||
.array(z.object({ name: z.string(), value: z.string() }))
|
||||
.nullable()
|
||||
.optional(),
|
||||
hcScheme: z.string().optional(),
|
||||
hcHostname: z.string(),
|
||||
hcPort: z
|
||||
.string()
|
||||
.min(1, { message: t("healthCheckPortInvalid") })
|
||||
.refine(
|
||||
(val) => {
|
||||
const port = parseInt(val);
|
||||
return port > 0 && port <= 65535;
|
||||
},
|
||||
{
|
||||
message: t("healthCheckPortInvalid")
|
||||
}
|
||||
),
|
||||
hcFollowRedirects: z.boolean(),
|
||||
hcMode: z.string(),
|
||||
hcUnhealthyInterval: z.int().positive().min(5),
|
||||
hcTlsServerName: z.string(),
|
||||
hcHealthyThreshold: z
|
||||
.int()
|
||||
.positive()
|
||||
.min(1, {
|
||||
message: t("healthCheckHealthyThresholdMin")
|
||||
}),
|
||||
hcUnhealthyThreshold: z
|
||||
.int()
|
||||
.positive()
|
||||
.min(1, {
|
||||
message: t("healthCheckUnhealthyThresholdMin")
|
||||
})
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.hcMode !== "tcp") {
|
||||
if (!data.hcPath || data.hcPath.length < 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t("healthCheckPathRequired"),
|
||||
path: ["hcPath"]
|
||||
});
|
||||
}
|
||||
if (!data.hcMethod || data.hcMethod.length < 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t("healthCheckMethodRequired"),
|
||||
path: ["hcMethod"]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof healthCheckSchema>>({
|
||||
resolver: zodResolver(healthCheckSchema),
|
||||
defaultValues: {}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const getDefaultScheme = () => {
|
||||
if (initialConfig?.hcScheme) {
|
||||
return initialConfig.hcScheme;
|
||||
}
|
||||
if (targetMethod === "https") {
|
||||
return "https";
|
||||
}
|
||||
return "http";
|
||||
};
|
||||
|
||||
form.reset({
|
||||
hcEnabled: initialConfig?.hcEnabled,
|
||||
hcPath: initialConfig?.hcPath,
|
||||
hcMethod: initialConfig?.hcMethod,
|
||||
hcInterval: initialConfig?.hcInterval,
|
||||
hcTimeout: initialConfig?.hcTimeout,
|
||||
hcStatus: initialConfig?.hcStatus,
|
||||
hcHeaders: initialConfig?.hcHeaders,
|
||||
hcScheme: getDefaultScheme(),
|
||||
hcHostname: initialConfig?.hcHostname,
|
||||
hcPort: initialConfig?.hcPort
|
||||
? initialConfig.hcPort.toString()
|
||||
: "",
|
||||
hcFollowRedirects: initialConfig?.hcFollowRedirects,
|
||||
hcMode: initialConfig?.hcMode ?? "http",
|
||||
hcUnhealthyInterval: initialConfig?.hcUnhealthyInterval,
|
||||
hcTlsServerName: initialConfig?.hcTlsServerName ?? "",
|
||||
hcHealthyThreshold: initialConfig?.hcHealthyThreshold ?? 1,
|
||||
hcUnhealthyThreshold: initialConfig?.hcUnhealthyThreshold ?? 1
|
||||
});
|
||||
}, [open]);
|
||||
|
||||
const handleFieldChange = async (fieldName: string, value: any) => {
|
||||
try {
|
||||
const currentValues = form.getValues();
|
||||
const updatedValues = { ...currentValues, [fieldName]: value };
|
||||
|
||||
const configToSend: HealthCheckConfig = {
|
||||
...updatedValues,
|
||||
hcPath: updatedValues.hcPath ?? "",
|
||||
hcMethod: updatedValues.hcMethod ?? "",
|
||||
hcPort: parseInt(updatedValues.hcPort),
|
||||
hcStatus: updatedValues.hcStatus || null,
|
||||
hcHealthyThreshold: updatedValues.hcHealthyThreshold,
|
||||
hcUnhealthyThreshold: updatedValues.hcUnhealthyThreshold
|
||||
};
|
||||
|
||||
await onChanges(configToSend);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t("healthCheckError"),
|
||||
description: t("healthCheckErrorDescription"),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Credenza open={open} onOpenChange={setOpen}>
|
||||
<CredenzaContent className="max-w-2xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{t("configureHealthCheck")}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t("configureHealthCheckDescription", {
|
||||
target: targetAddress
|
||||
})}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<Form {...form}>
|
||||
<form className="space-y-6">
|
||||
<HealthCheckFormFields
|
||||
form={form}
|
||||
onFieldChange={handleFieldChange}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<Button onClick={() => setOpen(false)}>{t("done")}</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
@@ -25,16 +25,19 @@ type HealthCheckFormFieldsProps = {
|
||||
form: UseFormReturn<any>;
|
||||
onFieldChange?: (fieldName: string, value: any) => void;
|
||||
showNameField?: boolean;
|
||||
hideEnabledField?: boolean;
|
||||
};
|
||||
|
||||
export function HealthCheckFormFields({
|
||||
form,
|
||||
onFieldChange,
|
||||
showNameField
|
||||
showNameField,
|
||||
hideEnabledField
|
||||
}: HealthCheckFormFieldsProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const watchedEnabled = form.watch("hcEnabled");
|
||||
const showFields = hideEnabledField || watchedEnabled;
|
||||
const watchedMode = form.watch("hcMode");
|
||||
|
||||
const handleChange = (fieldName: string, value: any, fieldOnChange: (v: any) => void) => {
|
||||
@@ -67,30 +70,32 @@ export function HealthCheckFormFields({
|
||||
)}
|
||||
|
||||
{/* Enable Health Checks */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hcEnabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("enableHealthChecks")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("enableHealthChecksDescription")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) =>
|
||||
handleChange("hcEnabled", value, field.onChange)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{!hideEnabledField && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hcEnabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("enableHealthChecks")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("enableHealthChecksDescription")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) =>
|
||||
handleChange("hcEnabled", value, field.onChange)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{watchedEnabled && (
|
||||
{showFields && (
|
||||
<div className="space-y-4">
|
||||
{/* Mode */}
|
||||
<FormField
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import StandaloneHealthCheckCredenza, {
|
||||
import HealthCheckCredenza, {
|
||||
HealthCheckRow
|
||||
} from "@app/components/StandaloneHealthCheckCredenza";
|
||||
} from "@app/components/HealthCheckCredenza";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { DataTable, ExtendedColumnDef } from "@app/components/ui/data-table";
|
||||
@@ -22,7 +22,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import HealthCheckDialog from "./HealthCheckDialog";
|
||||
|
||||
|
||||
type StandaloneHealthChecksTableProps = {
|
||||
orgId: string;
|
||||
@@ -266,7 +266,8 @@ export default function HealthChecksTable({
|
||||
/>
|
||||
)}
|
||||
|
||||
<StandaloneHealthCheckCredenza
|
||||
<HealthCheckCredenza
|
||||
mode="submit"
|
||||
open={credenzaOpen}
|
||||
setOpen={(val) => {
|
||||
setCredenzaOpen(val);
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { HealthCheckFormFields } from "@app/components/HealthCheckFormFields";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@/components/Credenza";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export type HealthCheckRow = {
|
||||
targetHealthCheckId: number;
|
||||
name: string;
|
||||
hcEnabled: boolean;
|
||||
hcHealth: "unknown" | "healthy" | "unhealthy";
|
||||
hcMode: string | null;
|
||||
hcHostname: string | null;
|
||||
hcPort: number | null;
|
||||
hcPath: string | null;
|
||||
hcScheme: string | null;
|
||||
hcMethod: string | null;
|
||||
hcInterval: number | null;
|
||||
hcUnhealthyInterval: number | null;
|
||||
hcTimeout: number | null;
|
||||
hcHeaders: string | null;
|
||||
hcFollowRedirects: boolean | null;
|
||||
hcStatus: number | null;
|
||||
hcTlsServerName: string | null;
|
||||
hcHealthyThreshold: number | null;
|
||||
hcUnhealthyThreshold: number | null;
|
||||
};
|
||||
|
||||
type StandaloneHealthCheckCredenzaProps = {
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
orgId: string;
|
||||
initialValues?: HealthCheckRow | null;
|
||||
onSaved: () => void;
|
||||
};
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
name: "",
|
||||
hcEnabled: true,
|
||||
hcMode: "http",
|
||||
hcScheme: "https",
|
||||
hcMethod: "GET",
|
||||
hcHostname: "",
|
||||
hcPort: "",
|
||||
hcPath: "/",
|
||||
hcInterval: 30,
|
||||
hcUnhealthyInterval: 30,
|
||||
hcTimeout: 5,
|
||||
hcHealthyThreshold: 1,
|
||||
hcUnhealthyThreshold: 1,
|
||||
hcFollowRedirects: true,
|
||||
hcTlsServerName: "",
|
||||
hcStatus: null as number | null,
|
||||
hcHeaders: [] as { name: string; value: string }[]
|
||||
};
|
||||
|
||||
export default function StandaloneHealthCheckCredenza({
|
||||
open,
|
||||
setOpen,
|
||||
orgId,
|
||||
initialValues,
|
||||
onSaved
|
||||
}: StandaloneHealthCheckCredenzaProps) {
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const healthCheckSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, { message: t("standaloneHcNameLabel") }),
|
||||
hcEnabled: z.boolean(),
|
||||
hcPath: z.string().optional(),
|
||||
hcMethod: z.string().optional(),
|
||||
hcInterval: z
|
||||
.int()
|
||||
.positive()
|
||||
.min(5, { message: t("healthCheckIntervalMin") }),
|
||||
hcTimeout: z
|
||||
.int()
|
||||
.positive()
|
||||
.min(1, { message: t("healthCheckTimeoutMin") }),
|
||||
hcStatus: z.int().positive().min(100).optional().nullable(),
|
||||
hcHeaders: z
|
||||
.array(z.object({ name: z.string(), value: z.string() }))
|
||||
.nullable()
|
||||
.optional(),
|
||||
hcScheme: z.string().optional(),
|
||||
hcHostname: z.string(),
|
||||
hcPort: z
|
||||
.string()
|
||||
.min(1, { message: t("healthCheckPortInvalid") })
|
||||
.refine(
|
||||
(val) => {
|
||||
const port = parseInt(val);
|
||||
return port > 0 && port <= 65535;
|
||||
},
|
||||
{ message: t("healthCheckPortInvalid") }
|
||||
),
|
||||
hcFollowRedirects: z.boolean(),
|
||||
hcMode: z.string(),
|
||||
hcUnhealthyInterval: z.int().positive().min(5),
|
||||
hcTlsServerName: z.string(),
|
||||
hcHealthyThreshold: z
|
||||
.int()
|
||||
.positive()
|
||||
.min(1, { message: t("healthCheckHealthyThresholdMin") }),
|
||||
hcUnhealthyThreshold: z
|
||||
.int()
|
||||
.positive()
|
||||
.min(1, { message: t("healthCheckUnhealthyThresholdMin") })
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.hcMode !== "tcp") {
|
||||
if (!data.hcPath || data.hcPath.length < 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t("healthCheckPathRequired"),
|
||||
path: ["hcPath"]
|
||||
});
|
||||
}
|
||||
if (!data.hcMethod || data.hcMethod.length < 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t("healthCheckMethodRequired"),
|
||||
path: ["hcMethod"]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof healthCheckSchema>;
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(healthCheckSchema),
|
||||
defaultValues: DEFAULT_VALUES
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
if (initialValues) {
|
||||
let parsedHeaders: { name: string; value: string }[] = [];
|
||||
if (initialValues.hcHeaders) {
|
||||
try {
|
||||
parsedHeaders = JSON.parse(initialValues.hcHeaders);
|
||||
} catch {
|
||||
parsedHeaders = [];
|
||||
}
|
||||
}
|
||||
|
||||
form.reset({
|
||||
name: initialValues.name,
|
||||
hcEnabled: initialValues.hcEnabled,
|
||||
hcMode: initialValues.hcMode ?? "http",
|
||||
hcScheme: initialValues.hcScheme ?? "https",
|
||||
hcMethod: initialValues.hcMethod ?? "GET",
|
||||
hcHostname: initialValues.hcHostname ?? "",
|
||||
hcPort: initialValues.hcPort
|
||||
? initialValues.hcPort.toString()
|
||||
: "",
|
||||
hcPath: initialValues.hcPath ?? "/",
|
||||
hcInterval: initialValues.hcInterval ?? 30,
|
||||
hcUnhealthyInterval: initialValues.hcUnhealthyInterval ?? 30,
|
||||
hcTimeout: initialValues.hcTimeout ?? 5,
|
||||
hcHealthyThreshold: initialValues.hcHealthyThreshold ?? 1,
|
||||
hcUnhealthyThreshold: initialValues.hcUnhealthyThreshold ?? 1,
|
||||
hcFollowRedirects: initialValues.hcFollowRedirects ?? true,
|
||||
hcTlsServerName: initialValues.hcTlsServerName ?? "",
|
||||
hcStatus: initialValues.hcStatus ?? null,
|
||||
hcHeaders: parsedHeaders
|
||||
});
|
||||
} else {
|
||||
form.reset(DEFAULT_VALUES);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload = {
|
||||
name: values.name,
|
||||
hcEnabled: values.hcEnabled,
|
||||
hcMode: values.hcMode,
|
||||
hcScheme: values.hcScheme,
|
||||
hcMethod: values.hcMethod,
|
||||
hcHostname: values.hcHostname,
|
||||
hcPort: parseInt(values.hcPort),
|
||||
hcPath: values.hcPath ?? "",
|
||||
hcInterval: values.hcInterval,
|
||||
hcUnhealthyInterval: values.hcUnhealthyInterval,
|
||||
hcTimeout: values.hcTimeout,
|
||||
hcHealthyThreshold: values.hcHealthyThreshold,
|
||||
hcUnhealthyThreshold: values.hcUnhealthyThreshold,
|
||||
hcFollowRedirects: values.hcFollowRedirects,
|
||||
hcTlsServerName: values.hcTlsServerName,
|
||||
hcStatus: values.hcStatus || null,
|
||||
hcHeaders:
|
||||
values.hcHeaders && values.hcHeaders.length > 0
|
||||
? JSON.stringify(values.hcHeaders)
|
||||
: null
|
||||
};
|
||||
|
||||
if (initialValues) {
|
||||
await api.post(
|
||||
`/org/${orgId}/health-check/${initialValues.targetHealthCheckId}`,
|
||||
payload
|
||||
);
|
||||
} else {
|
||||
await api.put(
|
||||
`/org/${orgId}/health-check`,
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
toast({ title: t("standaloneHcSaved") });
|
||||
onSaved();
|
||||
setOpen(false);
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isEditing = !!initialValues;
|
||||
|
||||
return (
|
||||
<Credenza open={open} onOpenChange={setOpen}>
|
||||
<CredenzaContent className="max-w-2xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{isEditing
|
||||
? t("standaloneHcEditTitle")
|
||||
: t("standaloneHcCreateTitle")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t("standaloneHcDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="standalone-hc-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6"
|
||||
>
|
||||
<HealthCheckFormFields form={form} showNameField />
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline" type="button">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
form="standalone-hc-form"
|
||||
disabled={loading}
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user