feat: Implement notification channel based on status (Service Dialog Form and Refactor Split ServiceForm into smaller components).
- The Service Dialog Form's Notification Channel field now respects the `notification_status`. If `notification_status` is enabled, the user can select one or multiple notification channel IDs.
This commit is contained in:
@@ -184,7 +184,7 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
|||||||
if (data?.agent_id && data.agent_id !== '1') {
|
if (data?.agent_id && data.agent_id !== '1') {
|
||||||
label = `${regionName} (${data.agent_id})`;
|
label = `${regionName} (${data.agent_id})`;
|
||||||
} else if (regionName === 'Default' && data?.agent_id === '1') {
|
} else if (regionName === 'Default' && data?.agent_id === '1') {
|
||||||
label = `Default (Agent 1)`;
|
label = `Default System Check (Agent 1)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorIndex = index % modernColors.length;
|
const colorIndex = index % modernColors.length;
|
||||||
|
|||||||
@@ -1,228 +1,3 @@
|
|||||||
|
|
||||||
import { Form } from "@/components/ui/form";
|
// Re-export the ServiceForm component from the add-service directory
|
||||||
import { useForm } from "react-hook-form";
|
export { ServiceForm } from './add-service/ServiceForm';
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { serviceSchema, ServiceFormData } from "./add-service/types";
|
|
||||||
import { ServiceBasicFields } from "./add-service/ServiceBasicFields";
|
|
||||||
import { ServiceTypeField } from "./add-service/ServiceTypeField";
|
|
||||||
import { ServiceConfigFields } from "./add-service/ServiceConfigFields";
|
|
||||||
import { ServiceNotificationFields } from "./add-service/ServiceNotificationFields";
|
|
||||||
import { ServiceFormActions } from "./add-service/ServiceFormActions";
|
|
||||||
import { serviceService } from "@/services/serviceService";
|
|
||||||
import { Service } from "@/types/service.types";
|
|
||||||
import { ServiceRegionalFields } from "./add-service/ServiceRegionalFields";
|
|
||||||
|
|
||||||
interface ServiceFormProps {
|
|
||||||
onSuccess: () => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
initialData?: Service | null;
|
|
||||||
isEdit?: boolean;
|
|
||||||
onSubmitStart?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ServiceForm({
|
|
||||||
onSuccess,
|
|
||||||
onCancel,
|
|
||||||
initialData,
|
|
||||||
isEdit = false,
|
|
||||||
onSubmitStart
|
|
||||||
}: ServiceFormProps) {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
|
|
||||||
// Initialize form with default values
|
|
||||||
const form = useForm<ServiceFormData>({
|
|
||||||
resolver: zodResolver(serviceSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
type: "http",
|
|
||||||
url: "",
|
|
||||||
port: "",
|
|
||||||
interval: "60",
|
|
||||||
retries: "3",
|
|
||||||
notificationChannel: "",
|
|
||||||
alertTemplate: "",
|
|
||||||
regionalMonitoringEnabled: false,
|
|
||||||
regionalAgent: "",
|
|
||||||
},
|
|
||||||
mode: "onBlur",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Populate form when initialData changes (separate from initialization)
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialData && isEdit) {
|
|
||||||
// Ensure the type is one of the allowed values
|
|
||||||
const serviceType = (initialData.type || "http").toLowerCase();
|
|
||||||
const validType = ["http", "ping", "tcp", "dns"].includes(serviceType)
|
|
||||||
? serviceType as "http" | "ping" | "tcp" | "dns"
|
|
||||||
: "http";
|
|
||||||
|
|
||||||
// For PING services, use host field; for DNS use domain field; for TCP use host field; others use url
|
|
||||||
let urlValue = "";
|
|
||||||
let portValue = "";
|
|
||||||
|
|
||||||
if (validType === "ping") {
|
|
||||||
urlValue = initialData.host || "";
|
|
||||||
} else if (validType === "dns") {
|
|
||||||
urlValue = initialData.domain || "";
|
|
||||||
} else if (validType === "tcp") {
|
|
||||||
urlValue = initialData.host || "";
|
|
||||||
portValue = String(initialData.port || "");
|
|
||||||
} else {
|
|
||||||
urlValue = initialData.url || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle regional monitoring data - check regional_status field
|
|
||||||
const isRegionalEnabled = initialData.regional_status === "enabled";
|
|
||||||
const regionalAgent = isRegionalEnabled && initialData.region_name && initialData.agent_id
|
|
||||||
? `${initialData.region_name}|${initialData.agent_id}`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// Reset the form with initial data values
|
|
||||||
form.reset({
|
|
||||||
name: initialData.name || "",
|
|
||||||
type: validType,
|
|
||||||
url: urlValue,
|
|
||||||
port: portValue,
|
|
||||||
interval: String(initialData.interval || 60),
|
|
||||||
retries: String(initialData.retries || 3),
|
|
||||||
notificationChannel: initialData.notificationChannel === "none" ? "" : initialData.notificationChannel || "",
|
|
||||||
alertTemplate: initialData.alertTemplate === "default" ? "" : initialData.alertTemplate || "",
|
|
||||||
regionalMonitoringEnabled: isRegionalEnabled,
|
|
||||||
regionalAgent: regionalAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log for debugging
|
|
||||||
console.log("Populating form with data:", {
|
|
||||||
type: validType,
|
|
||||||
url: urlValue,
|
|
||||||
port: portValue,
|
|
||||||
regionalAgent,
|
|
||||||
regionalMonitoringEnabled: isRegionalEnabled,
|
|
||||||
regional_status: initialData.regional_status,
|
|
||||||
region_name: initialData.region_name,
|
|
||||||
agent_id: initialData.agent_id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [initialData, isEdit, form]);
|
|
||||||
|
|
||||||
const handleSubmit = async (data: ServiceFormData) => {
|
|
||||||
if (isSubmitting) return;
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
if (onSubmitStart) onSubmitStart();
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("Form data being submitted:", data); // Debug log for submitted data
|
|
||||||
|
|
||||||
// Parse regional agent selection
|
|
||||||
let regionName = "";
|
|
||||||
let agentId = "";
|
|
||||||
let regionalStatus: "enabled" | "disabled" = "disabled";
|
|
||||||
|
|
||||||
// Set regional status and agent data based on form values
|
|
||||||
if (data.regionalMonitoringEnabled) {
|
|
||||||
regionalStatus = "enabled";
|
|
||||||
if (data.regionalAgent && data.regionalAgent !== "") {
|
|
||||||
const [parsedRegionName, parsedAgentId] = data.regionalAgent.split("|");
|
|
||||||
regionName = parsedRegionName || "";
|
|
||||||
agentId = parsedAgentId || "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare service data with proper field mapping
|
|
||||||
const serviceData = {
|
|
||||||
name: data.name,
|
|
||||||
type: data.type,
|
|
||||||
interval: parseInt(data.interval),
|
|
||||||
retries: parseInt(data.retries),
|
|
||||||
notificationChannel: data.notificationChannel === "none" ? "" : data.notificationChannel,
|
|
||||||
alertTemplate: data.alertTemplate === "default" ? "" : data.alertTemplate,
|
|
||||||
// Use regional_status field instead of regionalMonitoringEnabled
|
|
||||||
regionalStatus: regionalStatus,
|
|
||||||
regionName: regionName,
|
|
||||||
agentId: agentId,
|
|
||||||
// Map the URL field to appropriate database field based on service type
|
|
||||||
...(data.type === "dns"
|
|
||||||
? { domain: data.url, url: "", host: "", port: undefined } // DNS: store in domain field
|
|
||||||
: data.type === "ping"
|
|
||||||
? { host: data.url, url: "", domain: "", port: undefined } // PING: store in host field
|
|
||||||
: data.type === "tcp"
|
|
||||||
? { host: data.url, port: parseInt(data.port || "80"), url: "", domain: "" } // TCP: store in host and port fields
|
|
||||||
: { url: data.url, domain: "", host: "", port: undefined } // HTTP: store in url field
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("Service data being sent:", serviceData);
|
|
||||||
|
|
||||||
if (isEdit && initialData) {
|
|
||||||
// Update existing service
|
|
||||||
await serviceService.updateService(initialData.id, serviceData);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Service updated",
|
|
||||||
description: `${data.name} has been updated successfully.`,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Create new service
|
|
||||||
await serviceService.createService(serviceData);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Service created",
|
|
||||||
description: `${data.name} has been added to monitoring.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSuccess();
|
|
||||||
if (!isEdit) {
|
|
||||||
form.reset();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error ${isEdit ? 'updating' : 'creating'} service:`, error);
|
|
||||||
toast({
|
|
||||||
title: `Failed to ${isEdit ? 'update' : 'create'} service`,
|
|
||||||
description: `An error occurred while ${isEdit ? 'updating' : 'creating'} the service.`,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 pb-6">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Basic Information</h3>
|
|
||||||
<ServiceBasicFields form={form} />
|
|
||||||
<ServiceTypeField form={form} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Configuration</h3>
|
|
||||||
<ServiceConfigFields form={form} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Regional Monitoring</h3>
|
|
||||||
<ServiceRegionalFields form={form} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Notifications</h3>
|
|
||||||
<ServiceNotificationFields form={form} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ServiceFormActions
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
onCancel={onCancel}
|
|
||||||
submitLabel={isEdit ? "Update Service" : "Create Service"}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import { useForm } from "react-hook-form";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { serviceSchema, ServiceFormData } from "./types";
|
import { serviceSchema, ServiceFormData } from "./types";
|
||||||
import { ServiceBasicFields } from "./ServiceBasicFields";
|
import { ServiceBasicFields } from "./ServiceBasicFields";
|
||||||
import { ServiceTypeField } from "./ServiceTypeField";
|
import { ServiceTypeField } from "./ServiceTypeField";
|
||||||
@@ -14,6 +13,7 @@ import { ServiceFormActions } from "./ServiceFormActions";
|
|||||||
import { serviceService } from "@/services/serviceService";
|
import { serviceService } from "@/services/serviceService";
|
||||||
import { Service } from "@/types/service.types";
|
import { Service } from "@/types/service.types";
|
||||||
import { ServiceRegionalFields } from "./ServiceRegionalFields";
|
import { ServiceRegionalFields } from "./ServiceRegionalFields";
|
||||||
|
import { getServiceFormDefaults, mapServiceToFormData, mapFormDataToServiceData } from "./serviceFormUtils";
|
||||||
|
|
||||||
interface ServiceFormProps {
|
interface ServiceFormProps {
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
@@ -36,73 +36,28 @@ export function ServiceForm({
|
|||||||
// Initialize form with default values
|
// Initialize form with default values
|
||||||
const form = useForm<ServiceFormData>({
|
const form = useForm<ServiceFormData>({
|
||||||
resolver: zodResolver(serviceSchema),
|
resolver: zodResolver(serviceSchema),
|
||||||
defaultValues: {
|
defaultValues: getServiceFormDefaults(),
|
||||||
name: "",
|
|
||||||
type: "http",
|
|
||||||
url: "",
|
|
||||||
port: "",
|
|
||||||
interval: "60",
|
|
||||||
retries: "3",
|
|
||||||
notificationChannel: "",
|
|
||||||
alertTemplate: "",
|
|
||||||
regionalMonitoringEnabled: false,
|
|
||||||
regionalAgent: "",
|
|
||||||
},
|
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate form when initialData changes (separate from initialization)
|
// Populate form when initialData changes (separate from initialization)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData && isEdit) {
|
if (initialData && isEdit) {
|
||||||
// Ensure the type is one of the allowed values
|
const formData = mapServiceToFormData(initialData);
|
||||||
const serviceType = (initialData.type || "http").toLowerCase();
|
form.reset(formData);
|
||||||
const validType = ["http", "ping", "tcp", "dns"].includes(serviceType)
|
|
||||||
? serviceType as "http" | "ping" | "tcp" | "dns"
|
|
||||||
: "http";
|
|
||||||
|
|
||||||
// For PING services, use host field; for DNS use domain field; for TCP use host field; others use url
|
|
||||||
let urlValue = "";
|
|
||||||
let portValue = "";
|
|
||||||
|
|
||||||
if (validType === "ping") {
|
|
||||||
urlValue = initialData.host || "";
|
|
||||||
} else if (validType === "dns") {
|
|
||||||
urlValue = initialData.domain || "";
|
|
||||||
} else if (validType === "tcp") {
|
|
||||||
urlValue = initialData.host || "";
|
|
||||||
portValue = String(initialData.port || "");
|
|
||||||
} else {
|
|
||||||
urlValue = initialData.url || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle regional monitoring data - ensure proper assignment display
|
|
||||||
const regionalAgent = initialData.region_name && initialData.agent_id
|
|
||||||
? `${initialData.region_name}|${initialData.agent_id}`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// Reset the form with initial data values
|
|
||||||
form.reset({
|
|
||||||
name: initialData.name || "",
|
|
||||||
type: validType,
|
|
||||||
url: urlValue,
|
|
||||||
port: portValue,
|
|
||||||
interval: String(initialData.interval || 60),
|
|
||||||
retries: String(initialData.retries || 3),
|
|
||||||
notificationChannel: initialData.notificationChannel || "",
|
|
||||||
alertTemplate: initialData.alertTemplate || "",
|
|
||||||
regionalMonitoringEnabled: Boolean(initialData.regional_monitoring_enabled),
|
|
||||||
regionalAgent: regionalAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log for debugging
|
// Log for debugging
|
||||||
console.log("Populating form with data:", {
|
console.log("Populating form with data:", {
|
||||||
type: validType,
|
type: formData.type,
|
||||||
url: urlValue,
|
url: formData.url,
|
||||||
port: portValue,
|
port: formData.port,
|
||||||
regionalAgent,
|
regionalAgent: formData.regionalAgent,
|
||||||
regionalMonitoringEnabled: Boolean(initialData.regional_monitoring_enabled),
|
regionalMonitoringEnabled: formData.regionalMonitoringEnabled,
|
||||||
|
regional_status: initialData.regional_status,
|
||||||
region_name: initialData.region_name,
|
region_name: initialData.region_name,
|
||||||
agent_id: initialData.agent_id
|
agent_id: initialData.agent_id,
|
||||||
|
notification_status: initialData.notification_status,
|
||||||
|
notificationChannels: formData.notificationChannels
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [initialData, isEdit, form]);
|
}, [initialData, isEdit, form]);
|
||||||
@@ -114,42 +69,9 @@ export function ServiceForm({
|
|||||||
if (onSubmitStart) onSubmitStart();
|
if (onSubmitStart) onSubmitStart();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("Form data being submitted:", data); // Debug log for submitted data
|
console.log("Form data being submitted:", data);
|
||||||
|
|
||||||
// Parse regional agent selection
|
|
||||||
let regionName = "";
|
|
||||||
let agentId = "";
|
|
||||||
|
|
||||||
// Only set region and agent if regional monitoring is enabled AND an agent is selected (not unassign)
|
|
||||||
if (data.regionalMonitoringEnabled && data.regionalAgent && data.regionalAgent !== "") {
|
|
||||||
const [parsedRegionName, parsedAgentId] = data.regionalAgent.split("|");
|
|
||||||
regionName = parsedRegionName || "";
|
|
||||||
agentId = parsedAgentId || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare service data with proper field mapping
|
|
||||||
const serviceData = {
|
|
||||||
name: data.name,
|
|
||||||
type: data.type,
|
|
||||||
interval: parseInt(data.interval),
|
|
||||||
retries: parseInt(data.retries),
|
|
||||||
notificationChannel: data.notificationChannel || undefined,
|
|
||||||
alertTemplate: data.alertTemplate || undefined,
|
|
||||||
regionalMonitoringEnabled: data.regionalMonitoringEnabled || false,
|
|
||||||
// Always set region_name and agent_id - empty strings when unassigned
|
|
||||||
regionName: regionName,
|
|
||||||
agentId: agentId,
|
|
||||||
// Map the URL field to appropriate database field based on service type
|
|
||||||
...(data.type === "dns"
|
|
||||||
? { domain: data.url, url: "", host: "", port: undefined } // DNS: store in domain field
|
|
||||||
: data.type === "ping"
|
|
||||||
? { host: data.url, url: "", domain: "", port: undefined } // PING: store in host field
|
|
||||||
: data.type === "tcp"
|
|
||||||
? { host: data.url, port: parseInt(data.port || "80"), url: "", domain: "" } // TCP: store in host and port fields
|
|
||||||
: { url: data.url, domain: "", host: "", port: undefined } // HTTP: store in url field
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const serviceData = mapFormDataToServiceData(data);
|
||||||
console.log("Service data being sent:", serviceData);
|
console.log("Service data being sent:", serviceData);
|
||||||
|
|
||||||
if (isEdit && initialData) {
|
if (isEdit && initialData) {
|
||||||
@@ -188,7 +110,7 @@ export function ServiceForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 pb-4">
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 pb-6">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Basic Information</h3>
|
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Basic Information</h3>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
|
||||||
import { FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form";
|
import { FormControl, FormField, FormItem, FormLabel, FormDescription } from "@/components/ui/form";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { X } from "lucide-react";
|
||||||
import { UseFormReturn } from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import { ServiceFormData } from "./types";
|
import { ServiceFormData } from "./types";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { templateService } from "@/services/templateService";
|
|
||||||
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
@@ -16,11 +18,13 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
||||||
|
|
||||||
// Get the current form values for debugging
|
// Get the current form values for debugging
|
||||||
const notificationChannel = form.watch("notificationChannel");
|
const notificationStatus = form.watch("notificationStatus");
|
||||||
|
const notificationChannels = form.watch("notificationChannels") || [];
|
||||||
const alertTemplate = form.watch("alertTemplate");
|
const alertTemplate = form.watch("alertTemplate");
|
||||||
|
|
||||||
console.log("Current notification values:", {
|
console.log("Current notification values:", {
|
||||||
notificationChannel,
|
notificationStatus,
|
||||||
|
notificationChannels,
|
||||||
alertTemplate
|
alertTemplate
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,12 +34,6 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
queryFn: () => alertConfigService.getAlertConfigurations(),
|
queryFn: () => alertConfigService.getAlertConfigurations(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch templates for template selection
|
|
||||||
const { data: templates } = useQuery({
|
|
||||||
queryKey: ['templates'],
|
|
||||||
queryFn: () => templateService.getTemplates(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update alert configs when data is loaded
|
// Update alert configs when data is loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (alertConfigsData) {
|
if (alertConfigsData) {
|
||||||
@@ -51,43 +49,101 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
// Log when form values change to debug
|
// Log when form values change to debug
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("Notification values changed:", {
|
console.log("Notification values changed:", {
|
||||||
notificationChannel: form.getValues("notificationChannel"),
|
notificationStatus: form.getValues("notificationStatus"),
|
||||||
alertTemplate: form.getValues("alertTemplate")
|
notificationChannels: form.getValues("notificationChannels")
|
||||||
});
|
});
|
||||||
}, [form.watch("notificationChannel"), form.watch("alertTemplate")]);
|
}, [form.watch("notificationStatus"), form.watch("notificationChannels")]);
|
||||||
|
|
||||||
|
const handleChannelAdd = (channelId: string) => {
|
||||||
|
const currentChannels = form.getValues("notificationChannels") || [];
|
||||||
|
if (!currentChannels.includes(channelId)) {
|
||||||
|
form.setValue("notificationChannels", [...currentChannels, channelId]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChannelRemove = (channelId: string) => {
|
||||||
|
const currentChannels = form.getValues("notificationChannels") || [];
|
||||||
|
form.setValue("notificationChannels", currentChannels.filter(id => id !== channelId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedChannelNames = () => {
|
||||||
|
return (notificationChannels || []).map(channelId => {
|
||||||
|
const config = alertConfigs.find(c => c.id === channelId);
|
||||||
|
return config ? `${config.notify_name} (${config.notification_type})` : channelId;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="notificationChannel"
|
name="notificationStatus"
|
||||||
render={({ field }) => {
|
render={({ field }) => (
|
||||||
// Important: We need to preserve the actual value for notification channel
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
const fieldValue = field.value || "";
|
<div className="space-y-0.5">
|
||||||
const displayValue = fieldValue === "" ? "none" : fieldValue;
|
<FormLabel className="text-base">
|
||||||
|
Enable Notifications
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Enable or disable notifications for this service
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value === "enabled"}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
field.onChange(checked ? "enabled" : "disabled");
|
||||||
|
// Clear notification channels when disabled
|
||||||
|
if (!checked) {
|
||||||
|
form.setValue("notificationChannels", []);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
console.log("Rendering notification channel field with value:", {
|
<FormField
|
||||||
fieldValue,
|
control={form.control}
|
||||||
displayValue
|
name="notificationChannels"
|
||||||
});
|
render={({ field }) => (
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Notification Channel</FormLabel>
|
<FormLabel>Notification Channels</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{notificationStatus === "enabled"
|
||||||
|
? "Select notification channels for this service"
|
||||||
|
: "Enable notifications first to select channels"}
|
||||||
|
</FormDescription>
|
||||||
|
|
||||||
|
{/* Display selected channels as badges */}
|
||||||
|
{notificationChannels && notificationChannels.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{getSelectedChannelNames().map((channelName, index) => (
|
||||||
|
<Badge key={notificationChannels[index]} variant="secondary" className="flex items-center gap-1">
|
||||||
|
{channelName}
|
||||||
|
<X
|
||||||
|
className="h-3 w-3 cursor-pointer"
|
||||||
|
onClick={() => handleChannelRemove(notificationChannels[index])}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) => {
|
onValueChange={handleChannelAdd}
|
||||||
console.log("Notification channel changed to:", value);
|
disabled={notificationStatus !== "enabled"}
|
||||||
field.onChange(value === "none" ? "" : value);
|
value="" // Always reset to empty after selection
|
||||||
}}
|
|
||||||
value={displayValue}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
|
||||||
<SelectValue placeholder="Select a notification channel" />
|
<SelectValue placeholder="Add a notification channel" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
{alertConfigs
|
||||||
{alertConfigs.map((config) => (
|
.filter(config => !notificationChannels?.includes(config.id || ""))
|
||||||
|
.map((config) => (
|
||||||
<SelectItem key={config.id} value={config.id || ""}>
|
<SelectItem key={config.id} value={config.id || ""}>
|
||||||
{config.notify_name} ({config.notification_type})
|
{config.notify_name} ({config.notification_type})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -96,8 +152,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
@@ -118,20 +173,22 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
|
|||||||
field.onChange(value === "default" ? "" : value);
|
field.onChange(value === "default" ? "" : value);
|
||||||
}}
|
}}
|
||||||
value={displayValue}
|
value={displayValue}
|
||||||
|
disabled={notificationStatus !== "enabled"}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
|
||||||
<SelectValue placeholder="Select an alert template" />
|
<SelectValue placeholder="Select an alert template" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="default">Default</SelectItem>
|
<SelectItem value="default">Default</SelectItem>
|
||||||
{templates?.map((template) => (
|
{/* Add templates here when available */}
|
||||||
<SelectItem key={template.id} value={template.id}>
|
|
||||||
{template.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{notificationStatus === "enabled"
|
||||||
|
? "Choose a template for alert messages"
|
||||||
|
: "Enable notifications first to select template"}
|
||||||
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
|
||||||
|
import { Service } from "@/types/service.types";
|
||||||
|
import { ServiceFormData } from "./types";
|
||||||
|
|
||||||
|
export const getServiceFormDefaults = (): ServiceFormData => ({
|
||||||
|
name: "",
|
||||||
|
type: "http",
|
||||||
|
url: "",
|
||||||
|
port: "",
|
||||||
|
interval: "60",
|
||||||
|
retries: "3",
|
||||||
|
notificationStatus: "disabled",
|
||||||
|
notificationChannels: [],
|
||||||
|
alertTemplate: "",
|
||||||
|
regionalMonitoringEnabled: false,
|
||||||
|
regionalAgent: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mapServiceToFormData = (service: Service): ServiceFormData => {
|
||||||
|
// Ensure the type is one of the allowed values
|
||||||
|
const serviceType = (service.type || "http").toLowerCase();
|
||||||
|
const validType = ["http", "ping", "tcp", "dns"].includes(serviceType)
|
||||||
|
? serviceType as "http" | "ping" | "tcp" | "dns"
|
||||||
|
: "http";
|
||||||
|
|
||||||
|
// For PING services, use host field; for DNS use domain field; for TCP use host field; others use url
|
||||||
|
let urlValue = "";
|
||||||
|
let portValue = "";
|
||||||
|
|
||||||
|
if (validType === "ping") {
|
||||||
|
urlValue = service.host || "";
|
||||||
|
} else if (validType === "dns") {
|
||||||
|
urlValue = service.domain || "";
|
||||||
|
} else if (validType === "tcp") {
|
||||||
|
urlValue = service.host || "";
|
||||||
|
portValue = String(service.port || "");
|
||||||
|
} else {
|
||||||
|
urlValue = service.url || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle regional monitoring data - check regional_status field
|
||||||
|
const isRegionalEnabled = service.regional_status === "enabled";
|
||||||
|
const regionalAgent = isRegionalEnabled && service.region_name && service.agent_id
|
||||||
|
? `${service.region_name}|${service.agent_id}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// Handle notification channels - convert notification_channel and notificationChannel to array
|
||||||
|
const notificationChannels: string[] = [];
|
||||||
|
|
||||||
|
// Check for notification_channel field (from database)
|
||||||
|
if (service.notification_channel) {
|
||||||
|
notificationChannels.push(service.notification_channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for notificationChannel field (backward compatibility)
|
||||||
|
if (service.notificationChannel && !notificationChannels.includes(service.notificationChannel)) {
|
||||||
|
notificationChannels.push(service.notificationChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Mapping service to form data:", {
|
||||||
|
serviceName: service.name,
|
||||||
|
notification_status: service.notification_status,
|
||||||
|
notification_channel: service.notification_channel,
|
||||||
|
notificationChannel: service.notificationChannel,
|
||||||
|
mappedChannels: notificationChannels
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: service.name || "",
|
||||||
|
type: validType,
|
||||||
|
url: urlValue,
|
||||||
|
port: portValue,
|
||||||
|
interval: String(service.interval || 60),
|
||||||
|
retries: String(service.retries || 3),
|
||||||
|
notificationStatus: service.notification_status || "disabled",
|
||||||
|
notificationChannels: notificationChannels,
|
||||||
|
alertTemplate: service.alertTemplate === "default" ? "" : service.alertTemplate || "",
|
||||||
|
regionalMonitoringEnabled: isRegionalEnabled,
|
||||||
|
regionalAgent: regionalAgent,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapFormDataToServiceData = (data: ServiceFormData) => {
|
||||||
|
// Parse regional agent selection
|
||||||
|
let regionName = "";
|
||||||
|
let agentId = "";
|
||||||
|
let regionalStatus: "enabled" | "disabled" = "disabled";
|
||||||
|
|
||||||
|
// Set regional status and agent data based on form values
|
||||||
|
if (data.regionalMonitoringEnabled) {
|
||||||
|
regionalStatus = "enabled";
|
||||||
|
if (data.regionalAgent && data.regionalAgent !== "") {
|
||||||
|
const [parsedRegionName, parsedAgentId] = data.regionalAgent.split("|");
|
||||||
|
regionName = parsedRegionName || "";
|
||||||
|
agentId = parsedAgentId || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare service data with proper field mapping
|
||||||
|
return {
|
||||||
|
name: data.name,
|
||||||
|
type: data.type,
|
||||||
|
interval: parseInt(data.interval),
|
||||||
|
retries: parseInt(data.retries),
|
||||||
|
notificationStatus: data.notificationStatus || "disabled",
|
||||||
|
notificationChannels: data.notificationChannels || [],
|
||||||
|
alertTemplate: data.alertTemplate === "default" ? "" : data.alertTemplate,
|
||||||
|
// Use regional_status field instead of regionalMonitoringEnabled
|
||||||
|
regionalStatus: regionalStatus,
|
||||||
|
regionName: regionName,
|
||||||
|
agentId: agentId,
|
||||||
|
// Map the URL field to appropriate database field based on service type
|
||||||
|
...(data.type === "dns"
|
||||||
|
? { domain: data.url, url: "", host: "", port: undefined } // DNS: store in domain field
|
||||||
|
: data.type === "ping"
|
||||||
|
? { host: data.url, url: "", domain: "", port: undefined } // PING: store in host field
|
||||||
|
: data.type === "tcp"
|
||||||
|
? { host: data.url, port: parseInt(data.port || "80"), url: "", domain: "" } // TCP: store in host and port fields
|
||||||
|
: { url: data.url, domain: "", host: "", port: undefined } // HTTP: store in url field
|
||||||
|
)
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -11,7 +11,8 @@ export const serviceSchema = z.object({
|
|||||||
port: z.string().optional(),
|
port: z.string().optional(),
|
||||||
interval: z.string(),
|
interval: z.string(),
|
||||||
retries: z.string(),
|
retries: z.string(),
|
||||||
notificationChannel: z.string().optional(),
|
notificationStatus: z.enum(["enabled", "disabled"]).optional(),
|
||||||
|
notificationChannels: z.array(z.string()).optional(),
|
||||||
alertTemplate: z.string().optional(),
|
alertTemplate: z.string().optional(),
|
||||||
// Regional monitoring fields
|
// Regional monitoring fields
|
||||||
regionalMonitoringEnabled: z.boolean().optional(),
|
regionalMonitoringEnabled: z.boolean().optional(),
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const useConsolidatedUptimeData = ({ serviceId, serviceType, status, inte
|
|||||||
console.log(`Found default monitoring: ${sourceName} for normalized timestamp ${normalizedTimestamp}`);
|
console.log(`Found default monitoring: ${sourceName} for normalized timestamp ${normalizedTimestamp}`);
|
||||||
} else {
|
} else {
|
||||||
// Default monitoring fallback
|
// Default monitoring fallback
|
||||||
sourceName = 'Default (Agent 1)';
|
sourceName = 'Default System Check (Agent 1)';
|
||||||
isDefault = true;
|
isDefault = true;
|
||||||
console.log(`Using fallback default monitoring for normalized timestamp ${normalizedTimestamp}`);
|
console.log(`Using fallback default monitoring for normalized timestamp ${normalizedTimestamp}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ export * from './ServicesTableView';
|
|||||||
export * from './ServiceDeleteDialog';
|
export * from './ServiceDeleteDialog';
|
||||||
export * from './ServiceHistoryDialog';
|
export * from './ServiceHistoryDialog';
|
||||||
export * from './ServiceEditDialog';
|
export * from './ServiceEditDialog';
|
||||||
|
export * from './ServiceForm';
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { pb } from '@/lib/pocketbase';
|
import { pb } from '@/lib/pocketbase';
|
||||||
import { Service, CreateServiceParams, UptimeData } from '@/types/service.types';
|
import { Service, CreateServiceParams, UptimeData } from '@/types/service.types';
|
||||||
import { monitoringService } from './monitoring';
|
import { monitoringService } from './monitoring';
|
||||||
@@ -28,6 +27,7 @@ export const serviceService = {
|
|||||||
interval: item.heartbeat_interval || item.interval || 60,
|
interval: item.heartbeat_interval || item.interval || 60,
|
||||||
retries: item.max_retries || item.retries || 3,
|
retries: item.max_retries || item.retries || 3,
|
||||||
notificationChannel: item.notification_id,
|
notificationChannel: item.notification_id,
|
||||||
|
notification_status: item.notification_status || "disabled",
|
||||||
alertTemplate: item.template_id,
|
alertTemplate: item.template_id,
|
||||||
muteAlerts: item.alerts === "muted", // Convert string to boolean for compatibility
|
muteAlerts: item.alerts === "muted", // Convert string to boolean for compatibility
|
||||||
alerts: item.alerts || "unmuted", // Store actual database field
|
alerts: item.alerts || "unmuted", // Store actual database field
|
||||||
@@ -44,7 +44,7 @@ export const serviceService = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async createService(params: CreateServiceParams): Promise<Service> {
|
async createService(params: any): Promise<Service> {
|
||||||
try {
|
try {
|
||||||
// Convert service type to lowercase to avoid validation issues
|
// Convert service type to lowercase to avoid validation issues
|
||||||
const serviceType = params.type.toLowerCase();
|
const serviceType = params.type.toLowerCase();
|
||||||
@@ -61,7 +61,10 @@ export const serviceService = {
|
|||||||
last_checked: new Date().toLocaleString(),
|
last_checked: new Date().toLocaleString(),
|
||||||
heartbeat_interval: params.interval,
|
heartbeat_interval: params.interval,
|
||||||
max_retries: params.retries,
|
max_retries: params.retries,
|
||||||
notification_id: params.notificationChannel,
|
notification_status: params.notificationStatus || "disabled",
|
||||||
|
notification_id: params.notificationChannels && params.notificationChannels.length > 0
|
||||||
|
? params.notificationChannels[0] // Store first channel for backward compatibility
|
||||||
|
: null,
|
||||||
template_id: params.alertTemplate,
|
template_id: params.alertTemplate,
|
||||||
// Regional monitoring fields - use regional_status
|
// Regional monitoring fields - use regional_status
|
||||||
regional_status: params.regionalStatus || "disabled",
|
regional_status: params.regionalStatus || "disabled",
|
||||||
@@ -98,6 +101,7 @@ export const serviceService = {
|
|||||||
interval: record.heartbeat_interval || 60,
|
interval: record.heartbeat_interval || 60,
|
||||||
retries: record.max_retries || 3,
|
retries: record.max_retries || 3,
|
||||||
notificationChannel: record.notification_id,
|
notificationChannel: record.notification_id,
|
||||||
|
notification_status: record.notification_status || "disabled",
|
||||||
alertTemplate: record.template_id,
|
alertTemplate: record.template_id,
|
||||||
regional_status: record.regional_status || "disabled",
|
regional_status: record.regional_status || "disabled",
|
||||||
regional_monitoring_enabled: record.regional_status === "enabled",
|
regional_monitoring_enabled: record.regional_status === "enabled",
|
||||||
@@ -115,7 +119,7 @@ export const serviceService = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateService(id: string, params: CreateServiceParams): Promise<Service> {
|
async updateService(id: string, params: any): Promise<Service> {
|
||||||
try {
|
try {
|
||||||
// Convert service type to lowercase to avoid validation issues
|
// Convert service type to lowercase to avoid validation issues
|
||||||
const serviceType = params.type.toLowerCase();
|
const serviceType = params.type.toLowerCase();
|
||||||
@@ -128,7 +132,10 @@ export const serviceService = {
|
|||||||
service_type: serviceType,
|
service_type: serviceType,
|
||||||
heartbeat_interval: params.interval,
|
heartbeat_interval: params.interval,
|
||||||
max_retries: params.retries,
|
max_retries: params.retries,
|
||||||
notification_id: params.notificationChannel || null,
|
notification_status: params.notificationStatus || "disabled",
|
||||||
|
notification_id: params.notificationChannels && params.notificationChannels.length > 0
|
||||||
|
? params.notificationChannels[0] // Store first channel for backward compatibility
|
||||||
|
: null,
|
||||||
template_id: params.alertTemplate || null,
|
template_id: params.alertTemplate || null,
|
||||||
// Regional monitoring fields - use regional_status
|
// Regional monitoring fields - use regional_status
|
||||||
regional_status: params.regionalStatus || "disabled",
|
regional_status: params.regionalStatus || "disabled",
|
||||||
@@ -172,6 +179,7 @@ export const serviceService = {
|
|||||||
interval: record.heartbeat_interval || 60,
|
interval: record.heartbeat_interval || 60,
|
||||||
retries: record.max_retries || 3,
|
retries: record.max_retries || 3,
|
||||||
notificationChannel: record.notification_id,
|
notificationChannel: record.notification_id,
|
||||||
|
notification_status: record.notification_status || "disabled",
|
||||||
alertTemplate: record.template_id,
|
alertTemplate: record.template_id,
|
||||||
regional_status: record.regional_status || "disabled",
|
regional_status: record.regional_status || "disabled",
|
||||||
regional_monitoring_enabled: record.regional_status === "enabled",
|
regional_monitoring_enabled: record.regional_status === "enabled",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface Service {
|
|||||||
updated?: string;
|
updated?: string;
|
||||||
notification_channel?: string;
|
notification_channel?: string;
|
||||||
notificationChannel?: string; // Keep for backward compatibility
|
notificationChannel?: string; // Keep for backward compatibility
|
||||||
|
notification_status?: "enabled" | "disabled"; // Add notification_status field
|
||||||
alertTemplate?: string;
|
alertTemplate?: string;
|
||||||
alerts?: "muted" | "unmuted"; // Make sure alerts is properly typed as union
|
alerts?: "muted" | "unmuted"; // Make sure alerts is properly typed as union
|
||||||
muteAlerts?: boolean; // Keep this to avoid breaking existing code
|
muteAlerts?: boolean; // Keep this to avoid breaking existing code
|
||||||
|
|||||||
Reference in New Issue
Block a user