import React, { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Separator } from '@/components/ui/separator'; import { Info, Clock, Settings, ListFilter, EyeIcon, CalendarDays, RefreshCw, AlertTriangle } from 'lucide-react'; import { DynamicGroupRule, LdapConnection } from '@shared/schema'; import CronJobBuilder, { ScheduleItem } from './cron-job-builder'; import ConditionBuilder from './condition-builder'; import VariableSelector from './variable-selector'; import { useToast } from '@/hooks/use-toast'; import { queryClient, apiRequest } from '@/lib/queryClient'; import { useMutation, useQuery } from '@tanstack/react-query'; export interface Condition { id?: number; ruleId?: number; parentId?: number | null; attribute: string; customAttribute?: string; operator: string; value: string; logicalOperator?: string | null; isGroup?: boolean; } interface RuleEditorProps { rule?: DynamicGroupRule; onSave: (rule: DynamicGroupRule, schedules: ScheduleItem[]) => void; onCancel: () => void; } export const RuleEditor: React.FC = ({ rule, onSave, onCancel }) => { const { toast } = useToast(); const [activeTab, setActiveTab] = useState('general'); const [conditions, setConditions] = useState([]); const [schedules, setSchedules] = useState([]); const [formData, setFormData] = useState<{ name: string; description: string; targetGroupName: string; targetOU: string; createGroupIfNotExists: boolean; createOUIfNotExists: boolean; createGroupForEachAttributeValue: boolean; createOUForEachAttributeValue: boolean; enabled: boolean; variablePattern: string; connections: number[]; }>({ name: rule?.name || '', description: rule?.description || '', targetGroupName: rule?.targetGroup ? rule?.targetGroup.split(',')[0].replace('CN=', '') : '', targetOU: rule?.targetGroup ? rule?.targetGroup.split(',').slice(1).join(',') : '', createGroupIfNotExists: rule?.createGroupIfNotExists || false, createOUIfNotExists: rule?.createOUIfNotExists || false, createGroupForEachAttributeValue: rule?.createGroupForEachAttributeValue || false, createOUForEachAttributeValue: rule?.createOUForEachAttributeValue || false, enabled: rule?.enabled ?? true, variablePattern: rule?.variablePattern || '', connections: rule?.connectionIds || [] }); // Load LDAP connections const { data: ldapConnections = [] } = useQuery({ queryKey: ['/api/ldap-connections'], staleTime: 60000, }); // Load conditions for the rule if editing const { data: conditionsData } = useQuery({ queryKey: ['/api/dynamic-group-rules', rule?.id, 'conditions'], enabled: !!rule?.id, staleTime: 60000, }); // Load schedules for the rule if editing const { data: schedulesData } = useQuery({ queryKey: ['/api/dynamic-group-rules', rule?.id, 'schedules'], enabled: !!rule?.id, staleTime: 60000, }); // Initialize conditions and schedules when data loads useEffect(() => { if (conditionsData) { setConditions(conditionsData); } }, [conditionsData]); useEffect(() => { if (schedulesData) { setSchedules(schedulesData); } }, [schedulesData]); // Handle form input changes const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; // Handle toggle changes const handleToggleChange = (field: string, value: boolean) => { setFormData(prev => ({ ...prev, [field]: value })); }; // Handle connection selection const handleConnectionChange = (connectionIds: number[]) => { setFormData(prev => ({ ...prev, connections: connectionIds })); }; // Compute the full target group DN // Get the rootDSE string based on the first selected LDAP connection const getRootDSE = () => { if (!formData.connections || formData.connections.length === 0) { return "DC=example,DC=com"; // Default if no connection selected } const selectedConnection = ldapConnections.find(conn => formData.connections.includes(conn.id) ); if (!selectedConnection || !selectedConnection.domain) { return "DC=example,DC=com"; // Fallback if no domain info } const domainParts = selectedConnection.domain.split('.'); return domainParts.map(part => `DC=${part}`).join(','); }; // Ensure OU path has rootDSE appended const ensureRootDSE = (ouPath: string) => { if (!ouPath) return ''; // Check if the path already contains DC= if (ouPath.includes('DC=')) { return ouPath; } // Ensure a comma between the OU path and rootDSE if needed const separator = ouPath.endsWith(',') ? '' : ','; return `${ouPath}${separator}${getRootDSE()}`; }; const getTargetGroupDN = () => { if (!formData.targetGroupName || !formData.targetOU) return ''; // Ensure targetOU has rootDSE const fullOU = ensureRootDSE(formData.targetOU); return `CN=${formData.targetGroupName},${fullOU}`; }; // Handle form submission const handleSubmit = () => { if (!formData.name) { toast({ title: "Missing Information", description: "Please provide a name for the rule", variant: "destructive" }); setActiveTab('general'); return; } if (!formData.targetGroupName) { toast({ title: "Missing Information", description: "Please specify a target AD group name", variant: "destructive" }); setActiveTab('general'); return; } if (!formData.targetOU) { toast({ title: "Missing Information", description: "Please specify the organizational unit for the target group", variant: "destructive" }); setActiveTab('general'); return; } if (formData.connections.length === 0) { toast({ title: "Missing Information", description: "Please select at least one LDAP connection", variant: "destructive" }); setActiveTab('general'); return; } if (conditions.length === 0) { toast({ title: "Missing Information", description: "Please define at least one condition", variant: "destructive" }); setActiveTab('conditions'); return; } if (schedules.length === 0) { toast({ title: "Missing Information", description: "Please define at least one schedule", variant: "destructive" }); setActiveTab('schedules'); return; } const targetGroupDN = getTargetGroupDN(); const ruleData: DynamicGroupRule = { id: rule?.id, name: formData.name, description: formData.description || null, targetGroup: targetGroupDN, enabled: formData.enabled, createdAt: rule?.createdAt || new Date(), updatedAt: new Date(), lastRun: rule?.lastRun || null, lastRunStatus: rule?.lastRunStatus || null, variablePattern: formData.variablePattern || null, connectionIds: formData.connections, createGroupIfNotExists: formData.createGroupIfNotExists, createOUIfNotExists: formData.createOUIfNotExists, createGroupForEachAttributeValue: formData.createGroupForEachAttributeValue, createOUForEachAttributeValue: formData.createOUForEachAttributeValue }; onSave(ruleData, schedules); }; // Test the rule const testRuleMutation = useMutation({ mutationFn: async () => { const targetGroupDN = getTargetGroupDN(); const res = await apiRequest('POST', '/api/dynamic-group-rules/test', { rule: { name: formData.name, targetGroup: targetGroupDN, connectionIds: formData.connections, variablePattern: formData.variablePattern || null, createGroupIfNotExists: formData.createGroupIfNotExists, createOUIfNotExists: formData.createOUIfNotExists, createGroupForEachAttributeValue: formData.createGroupForEachAttributeValue, createOUForEachAttributeValue: formData.createOUForEachAttributeValue }, conditions }); return await res.json(); }, onSuccess: (data) => { toast({ title: "Test Results", description: `The rule would match ${data.matchCount} objects`, }); }, onError: (error: Error) => { toast({ title: "Test Failed", description: error.message, variant: "destructive" }); } }); const handleTest = () => { if (formData.connections.length === 0) { toast({ title: "Missing Information", description: "Please select at least one LDAP connection to test", variant: "destructive" }); return; } if (conditions.length === 0) { toast({ title: "Missing Information", description: "Please define at least one condition to test", variant: "destructive" }); return; } testRuleMutation.mutate(); }; return (
General Conditions Schedules Preview {/* General Settings Tab */} Rule Settings Configure the basic settings for your dynamic group rule
{ldapConnections.length === 0 ? (

No LDAP connections available. Please create one first.

) : (
{ldapConnections.map((connection) => (
{ if (checked) { handleConnectionChange([...formData.connections, connection.id]); } else { handleConnectionChange(formData.connections.filter(id => id !== connection.id)); } }} />
))}
)}

Select one or more LDAP connections to search for objects