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:
Tola Leng
2025-07-10 21:07:31 +07:00
parent ddeb22dc4d
commit 917d8a6d29
11 changed files with 272 additions and 384 deletions
@@ -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 const serviceData = mapFormDataToServiceData(data);
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
)
};
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,53 +49,110 @@ 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
console.log("Rendering notification channel field with value:", { </FormLabel>
fieldValue, <FormDescription>
displayValue Enable or disable notifications for this service
}); </FormDescription>
</div>
return ( <FormControl>
<FormItem> <Switch
<FormLabel>Notification Channel</FormLabel> checked={field.value === "enabled"}
<FormControl> onCheckedChange={(checked) => {
<Select field.onChange(checked ? "enabled" : "disabled");
onValueChange={(value) => { // Clear notification channels when disabled
console.log("Notification channel changed to:", value); if (!checked) {
field.onChange(value === "none" ? "" : value); form.setValue("notificationChannels", []);
}} }
value={displayValue} }}
> />
<SelectTrigger> </FormControl>
<SelectValue placeholder="Select a notification channel" /> </FormItem>
</SelectTrigger> )}
<SelectContent> />
<SelectItem value="none">None</SelectItem>
{alertConfigs.map((config) => ( <FormField
control={form.control}
name="notificationChannels"
render={({ field }) => (
<FormItem>
<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>
<Select
onValueChange={handleChannelAdd}
disabled={notificationStatus !== "enabled"}
value="" // Always reset to empty after selection
>
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
<SelectValue placeholder="Add a notification channel" />
</SelectTrigger>
<SelectContent>
{alertConfigs
.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>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</FormControl> </FormControl>
</FormItem> </FormItem>
); )}
}}
/> />
<FormField <FormField
@@ -118,24 +173,26 @@ 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,11 +11,12 @@ 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(),
regionalAgent: z.string().optional(), regionalAgent: z.string().optional(),
}); });
export type ServiceFormData = z.infer<typeof serviceSchema>; export type ServiceFormData = z.infer<typeof serviceSchema>;
@@ -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
View File
@@ -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"
+13 -5
View File
@@ -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",
+1
View File
@@ -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