feat: Allow multiple selections for regional agents.
- Allow multiple regional agents to be selected in the service dialog form, similar to the notification channels.
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { Form } from "@/components/ui/form";
|
import { Form } from "@/components/ui/form";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -47,18 +46,18 @@ export function ServiceForm({
|
|||||||
form.reset(formData);
|
form.reset(formData);
|
||||||
|
|
||||||
// Log for debugging
|
// Log for debugging
|
||||||
console.log("Populating form with data:", {
|
// console.log("Populating form with data:", {
|
||||||
type: formData.type,
|
// type: formData.type,
|
||||||
url: formData.url,
|
// url: formData.url,
|
||||||
port: formData.port,
|
// port: formData.port,
|
||||||
regionalAgent: formData.regionalAgent,
|
// regionalAgents: formData.regionalAgents,
|
||||||
regionalMonitoringEnabled: formData.regionalMonitoringEnabled,
|
// regionalMonitoringEnabled: formData.regionalMonitoringEnabled,
|
||||||
regional_status: initialData.regional_status,
|
// 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,
|
// notification_status: initialData.notification_status,
|
||||||
notificationChannels: formData.notificationChannels
|
// notificationChannels: formData.notificationChannels
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
}, [initialData, isEdit, form]);
|
}, [initialData, isEdit, form]);
|
||||||
|
|
||||||
@@ -97,7 +96,7 @@ export function ServiceForm({
|
|||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error ${isEdit ? 'updating' : 'creating'} service:`, error);
|
// console.error(`Error ${isEdit ? 'updating' : 'creating'} service:`, error);
|
||||||
toast({
|
toast({
|
||||||
title: `Failed to ${isEdit ? 'update' : 'create'} service`,
|
title: `Failed to ${isEdit ? 'update' : 'create'} service`,
|
||||||
description: `An error occurred while ${isEdit ? 'updating' : 'creating'} the service.`,
|
description: `An error occurred while ${isEdit ? 'updating' : 'creating'} the service.`,
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ 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 { regionalService } from "@/services/regionalService";
|
import { regionalService } from "@/services/regionalService";
|
||||||
import { MapPin, Loader2, X } from "lucide-react";
|
import { MapPin, Loader2, X, Plus } from "lucide-react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface ServiceRegionalFieldsProps {
|
interface ServiceRegionalFieldsProps {
|
||||||
form: UseFormReturn<ServiceFormData>;
|
form: UseFormReturn<ServiceFormData>;
|
||||||
@@ -14,7 +16,7 @@ interface ServiceRegionalFieldsProps {
|
|||||||
|
|
||||||
export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||||
const regionalMonitoringEnabled = form.watch("regionalMonitoringEnabled");
|
const regionalMonitoringEnabled = form.watch("regionalMonitoringEnabled");
|
||||||
const currentRegionalAgent = form.watch("regionalAgent");
|
const currentRegionalAgents = form.watch("regionalAgents") || [];
|
||||||
|
|
||||||
const { data: regionalAgents = [], isLoading } = useQuery({
|
const { data: regionalAgents = [], isLoading } = useQuery({
|
||||||
queryKey: ['regional-services'],
|
queryKey: ['regional-services'],
|
||||||
@@ -27,15 +29,16 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
|||||||
agent.connection === 'online' && agent.agent_id !== "1"
|
agent.connection === 'online' && agent.agent_id !== "1"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Find the current agent name for display
|
// Get available agents (not already selected)
|
||||||
const getCurrentAgentDisplay = () => {
|
const availableAgents = onlineAgents.filter(agent =>
|
||||||
if (!currentRegionalAgent || currentRegionalAgent === "unassign") {
|
!currentRegionalAgents.includes(`${agent.region_name}|${agent.agent_id}`)
|
||||||
return "Select a regional agent or unassign";
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const [regionName, agentId] = currentRegionalAgent.split("|");
|
// Get agent display name
|
||||||
|
const getAgentDisplayName = (agentValue: string) => {
|
||||||
|
const [regionName, agentId] = agentValue.split("|");
|
||||||
const agent = onlineAgents.find(agent =>
|
const agent = onlineAgents.find(agent =>
|
||||||
`${agent.region_name}|${agent.agent_id}` === currentRegionalAgent
|
`${agent.region_name}|${agent.agent_id}` === agentValue
|
||||||
);
|
);
|
||||||
|
|
||||||
if (agent) {
|
if (agent) {
|
||||||
@@ -43,25 +46,27 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If agent is not found in online agents, it might be offline but still assigned
|
// If agent is not found in online agents, it might be offline but still assigned
|
||||||
// Show the region name from the stored value
|
|
||||||
if (regionName && agentId) {
|
if (regionName && agentId) {
|
||||||
return `${regionName} (Agent ${agentId}) - Offline`;
|
return `${regionName} (Agent ${agentId}) - Offline`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "Select a regional agent or unassign";
|
return agentValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the proper select value - handle both assigned and unassigned cases
|
// Add regional agent
|
||||||
const getSelectValue = () => {
|
const addRegionalAgent = (agentValue: string) => {
|
||||||
if (!regionalMonitoringEnabled) {
|
if (agentValue && agentValue !== "select") {
|
||||||
return "unassign";
|
const currentAgents = form.getValues("regionalAgents") || [];
|
||||||
|
if (!currentAgents.includes(agentValue)) {
|
||||||
|
form.setValue("regionalAgents", [...currentAgents, agentValue]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!currentRegionalAgent || currentRegionalAgent === "") {
|
// Remove regional agent
|
||||||
return "unassign";
|
const removeRegionalAgent = (agentValue: string) => {
|
||||||
}
|
const currentAgents = form.getValues("regionalAgents") || [];
|
||||||
|
form.setValue("regionalAgents", currentAgents.filter(agent => agent !== agentValue));
|
||||||
return currentRegionalAgent;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -77,13 +82,19 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
|||||||
Regional Monitoring
|
Regional Monitoring
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Assign this service to a regional monitoring agent for distributed monitoring
|
Assign this service to regional monitoring agents for distributed monitoring
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value || false}
|
checked={field.value || false}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={(checked) => {
|
||||||
|
field.onChange(checked);
|
||||||
|
// Clear agents when disabling regional monitoring
|
||||||
|
if (!checked) {
|
||||||
|
form.setValue("regionalAgents", []);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -93,16 +104,36 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
|||||||
{regionalMonitoringEnabled && (
|
{regionalMonitoringEnabled && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="regionalAgent"
|
name="regionalAgents"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Regional Agent</FormLabel>
|
<FormLabel>Regional Agents</FormLabel>
|
||||||
|
|
||||||
|
{/* Display selected agents */}
|
||||||
|
{currentRegionalAgents.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
{currentRegionalAgents.map((agentValue) => (
|
||||||
|
<Badge key={agentValue} variant="secondary" className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
|
<span className="text-sm">{getAgentDisplayName(agentValue)}</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-4 w-4 p-0 ml-1"
|
||||||
|
onClick={() => removeRegionalAgent(agentValue)}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add new agent selector */}
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) => {
|
onValueChange={addRegionalAgent}
|
||||||
// Handle the unassign case by setting to empty string
|
value="select"
|
||||||
field.onChange(value === "unassign" ? "" : value);
|
|
||||||
}}
|
|
||||||
value={getSelectValue()}
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -110,7 +141,11 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
|||||||
<SelectValue placeholder={
|
<SelectValue placeholder={
|
||||||
isLoading
|
isLoading
|
||||||
? "Loading agents..."
|
? "Loading agents..."
|
||||||
: getCurrentAgentDisplay()
|
: availableAgents.length > 0
|
||||||
|
? "Select additional regional agents..."
|
||||||
|
: currentRegionalAgents.length > 0
|
||||||
|
? "All available agents selected"
|
||||||
|
: "No regional agents available"
|
||||||
} />
|
} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -122,47 +157,52 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
|||||||
Loading agents...
|
Loading agents...
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
) : availableAgents.length === 0 ? (
|
||||||
|
<SelectItem value="no-agents" disabled>
|
||||||
|
{currentRegionalAgents.length > 0
|
||||||
|
? "All available agents selected"
|
||||||
|
: "No online regional agents available"
|
||||||
|
}
|
||||||
|
</SelectItem>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<SelectItem value="unassign">
|
<SelectItem value="select" disabled>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<X className="h-4 w-4 text-red-500" />
|
<Plus className="h-4 w-4 text-muted-foreground" />
|
||||||
<span className="text-red-600">Unassign (No Regional Agent)</span>
|
<span className="text-muted-foreground">Select an agent to add...</span>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{onlineAgents.length === 0 ? (
|
{availableAgents.map((agent) => (
|
||||||
<SelectItem value="no-agents" disabled>
|
<SelectItem key={agent.id} value={`${agent.region_name}|${agent.agent_id}`}>
|
||||||
No online regional agents available
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
|
<span className="font-medium">{agent.region_name}</span>
|
||||||
|
<span className="text-muted-foreground">({agent.agent_ip_address})</span>
|
||||||
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
) : (
|
))}
|
||||||
onlineAgents.map((agent) => (
|
|
||||||
<SelectItem key={agent.id} value={`${agent.region_name}|${agent.agent_id}`}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
||||||
<span className="font-medium">{agent.region_name}</span>
|
|
||||||
<span className="text-muted-foreground">({agent.agent_ip_address})</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
||||||
{regionalMonitoringEnabled && onlineAgents.length === 0 && !isLoading && (
|
{regionalMonitoringEnabled && onlineAgents.length === 0 && !isLoading && (
|
||||||
<p className="text-sm text-amber-600">
|
<p className="text-sm text-amber-600">
|
||||||
No online regional agents found. Services will use default monitoring.
|
No online regional agents found. Services will use default monitoring.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{currentRegionalAgent && currentRegionalAgent !== "" && (
|
|
||||||
<p className="text-sm text-green-600">
|
{currentRegionalAgents.length === 0 && regionalMonitoringEnabled && (
|
||||||
Currently assigned to: {getCurrentAgentDisplay()}
|
<p className="text-sm text-orange-600">
|
||||||
|
No regional agents selected. Service will use default monitoring.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{(!currentRegionalAgent || currentRegionalAgent === "") && regionalMonitoringEnabled && (
|
|
||||||
<p className="text-sm text-orange-600">
|
{currentRegionalAgents.length > 0 && (
|
||||||
Service is unassigned and will use default monitoring.
|
<p className="text-sm text-green-600">
|
||||||
|
Service assigned to {currentRegionalAgents.length} regional agent{currentRegionalAgents.length > 1 ? 's' : ''}.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const getServiceFormDefaults = (): ServiceFormData => ({
|
|||||||
notificationChannels: [],
|
notificationChannels: [],
|
||||||
alertTemplate: "",
|
alertTemplate: "",
|
||||||
regionalMonitoringEnabled: false,
|
regionalMonitoringEnabled: false,
|
||||||
regionalAgent: "",
|
regionalAgents: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mapServiceToFormData = (service: Service): ServiceFormData => {
|
export const mapServiceToFormData = (service: Service): ServiceFormData => {
|
||||||
@@ -40,9 +40,12 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => {
|
|||||||
|
|
||||||
// Handle regional monitoring data - check regional_status field
|
// Handle regional monitoring data - check regional_status field
|
||||||
const isRegionalEnabled = service.regional_status === "enabled";
|
const isRegionalEnabled = service.regional_status === "enabled";
|
||||||
const regionalAgent = isRegionalEnabled && service.region_name && service.agent_id
|
const regionalAgents: string[] = [];
|
||||||
? `${service.region_name}|${service.agent_id}`
|
|
||||||
: "";
|
// For backward compatibility, if there's a single regional agent, add it to the array
|
||||||
|
if (isRegionalEnabled && service.region_name && service.agent_id) {
|
||||||
|
regionalAgents.push(`${service.region_name}|${service.agent_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle notification channels - convert notification_channel and notificationChannel to array
|
// Handle notification channels - convert notification_channel and notificationChannel to array
|
||||||
const notificationChannels: string[] = [];
|
const notificationChannels: string[] = [];
|
||||||
@@ -62,7 +65,8 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => {
|
|||||||
notification_status: service.notification_status,
|
notification_status: service.notification_status,
|
||||||
notification_channel: service.notification_channel,
|
notification_channel: service.notification_channel,
|
||||||
notificationChannel: service.notificationChannel,
|
notificationChannel: service.notificationChannel,
|
||||||
mappedChannels: notificationChannels
|
mappedChannels: notificationChannels,
|
||||||
|
regionalAgents: regionalAgents
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -76,21 +80,23 @@ export const mapServiceToFormData = (service: Service): ServiceFormData => {
|
|||||||
notificationChannels: notificationChannels,
|
notificationChannels: notificationChannels,
|
||||||
alertTemplate: service.alertTemplate === "default" ? "" : service.alertTemplate || "",
|
alertTemplate: service.alertTemplate === "default" ? "" : service.alertTemplate || "",
|
||||||
regionalMonitoringEnabled: isRegionalEnabled,
|
regionalMonitoringEnabled: isRegionalEnabled,
|
||||||
regionalAgent: regionalAgent,
|
regionalAgents: regionalAgents,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapFormDataToServiceData = (data: ServiceFormData) => {
|
export const mapFormDataToServiceData = (data: ServiceFormData) => {
|
||||||
// Parse regional agent selection
|
// Parse regional agent selection - for now, use the first agent for backward compatibility
|
||||||
let regionName = "";
|
let regionName = "";
|
||||||
let agentId = "";
|
let agentId = "";
|
||||||
let regionalStatus: "enabled" | "disabled" = "disabled";
|
let regionalStatus: "enabled" | "disabled" = "disabled";
|
||||||
|
|
||||||
// Set regional status and agent data based on form values
|
// Set regional status and agent data based on form values
|
||||||
if (data.regionalMonitoringEnabled) {
|
if (data.regionalMonitoringEnabled && data.regionalAgents && data.regionalAgents.length > 0) {
|
||||||
regionalStatus = "enabled";
|
regionalStatus = "enabled";
|
||||||
if (data.regionalAgent && data.regionalAgent !== "") {
|
// Use the first agent for backward compatibility with single-agent database schema
|
||||||
const [parsedRegionName, parsedAgentId] = data.regionalAgent.split("|");
|
const firstAgent = data.regionalAgents[0];
|
||||||
|
if (firstAgent && firstAgent !== "") {
|
||||||
|
const [parsedRegionName, parsedAgentId] = firstAgent.split("|");
|
||||||
regionName = parsedRegionName || "";
|
regionName = parsedRegionName || "";
|
||||||
agentId = parsedAgentId || "";
|
agentId = parsedAgentId || "";
|
||||||
}
|
}
|
||||||
@@ -109,6 +115,10 @@ export const mapFormDataToServiceData = (data: ServiceFormData) => {
|
|||||||
regionalStatus: regionalStatus,
|
regionalStatus: regionalStatus,
|
||||||
regionName: regionName,
|
regionName: regionName,
|
||||||
agentId: agentId,
|
agentId: agentId,
|
||||||
|
// Store multiple agents as a comment for future use (when backend supports it)
|
||||||
|
regionalAgentsNote: data.regionalAgents && data.regionalAgents.length > 1
|
||||||
|
? `Multiple agents selected: ${data.regionalAgents.join(', ')}`
|
||||||
|
: undefined,
|
||||||
// Map the URL field to appropriate database field based on service type
|
// Map the URL field to appropriate database field based on service type
|
||||||
...(data.type === "dns"
|
...(data.type === "dns"
|
||||||
? { domain: data.url, url: "", host: "", port: undefined } // DNS: store in domain field
|
? { domain: data.url, url: "", host: "", port: undefined } // DNS: store in domain field
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export const serviceSchema = z.object({
|
|||||||
notificationStatus: z.enum(["enabled", "disabled"]).optional(),
|
notificationStatus: z.enum(["enabled", "disabled"]).optional(),
|
||||||
notificationChannels: z.array(z.string()).optional(),
|
notificationChannels: z.array(z.string()).optional(),
|
||||||
alertTemplate: z.string().optional(),
|
alertTemplate: z.string().optional(),
|
||||||
// Regional monitoring fields
|
// Regional monitoring fields - now supports multiple agents
|
||||||
regionalMonitoringEnabled: z.boolean().optional(),
|
regionalMonitoringEnabled: z.boolean().optional(),
|
||||||
regionalAgent: z.string().optional(),
|
regionalAgents: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ServiceFormData = z.infer<typeof serviceSchema>;
|
export type ServiceFormData = z.infer<typeof serviceSchema>;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
@@ -5,7 +6,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
Reference in New Issue
Block a user