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:
Tola Leng
2025-07-10 23:06:10 +07:00
parent 7fd6d07c10
commit de79eb963d
6 changed files with 135 additions and 85 deletions
@@ -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"; );
}
// Get agent display name
const [regionName, agentId] = currentRegionalAgent.split("|"); 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 === "") {
return "unassign"; // Remove regional agent
} const removeRegionalAgent = (agentValue: string) => {
const currentAgents = form.getValues("regionalAgents") || [];
return currentRegionalAgent; form.setValue("regionalAgents", currentAgents.filter(agent => agent !== agentValue));
}; };
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 -1
View File
@@ -34,4 +34,4 @@ function Badge({ className, variant, ...props }: BadgeProps) {
) )
} }
export { Badge, badgeVariants } export { Badge, badgeVariants }
+3 -2
View File
@@ -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: {
@@ -53,4 +54,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
) )
Button.displayName = "Button" Button.displayName = "Button"
export { Button, buttonVariants } export { Button, buttonVariants }