import React, { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useToast } from "@/hooks/use-toast"; import { LdapConnection } from "@shared/schema"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Loader2, Plus, Trash } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; // Import shared types from our shared lib import { LdapOperator, LdapCondition, LdapAttribute } from "@/lib/ldap-types"; export type LdapQueryBuilderParams = { targetObject: "users" | "groups" | "computers" | "ous"; filter: LdapCondition; }; // Used for displaying the operators in the UI const operatorLabels: Record = { [LdapOperator.AND]: "AND (All conditions)", [LdapOperator.OR]: "OR (Any condition)", [LdapOperator.NOT]: "NOT", [LdapOperator.EQUALS]: "Equals", [LdapOperator.NOT_EQUALS]: "Does not equal", [LdapOperator.STARTS_WITH]: "Starts with", [LdapOperator.ENDS_WITH]: "Ends with", [LdapOperator.CONTAINS]: "Contains", [LdapOperator.GREATER_THAN]: "Greater than", [LdapOperator.LESS_THAN]: "Less than", [LdapOperator.PRESENT]: "Has value (attribute exists)", [LdapOperator.APPROX]: "Approximately equals", }; // Group operators by type for the UI const logicalOperators = [LdapOperator.AND, LdapOperator.OR, LdapOperator.NOT]; const comparisonOperators = [ LdapOperator.EQUALS, LdapOperator.NOT_EQUALS, LdapOperator.STARTS_WITH, LdapOperator.ENDS_WITH, LdapOperator.CONTAINS, LdapOperator.GREATER_THAN, LdapOperator.LESS_THAN, LdapOperator.PRESENT, LdapOperator.APPROX, ]; interface LdapQueryBuilderProps { connections: LdapConnection[]; value: LdapQueryBuilderParams; onChange: (value: LdapQueryBuilderParams) => void; onTest?: () => void; onSave?: () => void; } export function LdapQueryBuilder({ connections, value, onChange, onTest, onSave }: LdapQueryBuilderProps) { const { toast } = useToast(); const [selectedConnectionId, setSelectedConnectionId] = useState( connections.length > 0 ? connections[0].id : null ); // Load available attributes based on connection and object type const { data: availableAttributes = [], isLoading: isLoadingAttributes } = useQuery({ queryKey: ["/api/ldap-queries/attributes", { connectionId: selectedConnectionId, targetObject: value.targetObject }], enabled: !!selectedConnectionId && !!value.targetObject, }); // Update the selected connection ID if the connections list changes useEffect(() => { if (connections.length > 0 && !selectedConnectionId) { setSelectedConnectionId(connections[0].id); } }, [connections, selectedConnectionId]); // Handle changes to the query target object type const handleTargetObjectChange = (targetObject: "users" | "groups" | "computers" | "ous") => { onChange({ ...value, targetObject, }); }; // Handle updates to a condition const handleConditionChange = ( condition: LdapCondition, path: number[] = [] ): LdapCondition => { if (path.length === 0) { return condition; } const [index, ...restPath] = path; const updatedConditions = [...(value.filter.conditions || [])]; if (restPath.length === 0) { updatedConditions[index] = condition; } else { updatedConditions[index] = handleConditionChange( updatedConditions[index], restPath ); } return { ...value.filter, conditions: updatedConditions, }; }; // Add a new condition to a logical operator const handleAddCondition = (path: number[] = []) => { let currentCondition = value.filter; let parent = currentCondition; // Navigate to the target condition for (const index of path) { if (!currentCondition.conditions) { currentCondition.conditions = []; } parent = currentCondition; currentCondition = currentCondition.conditions[index]; } // Add a new condition if (!parent.conditions) { parent.conditions = []; } parent.conditions.push({ operator: LdapOperator.EQUALS, attribute: availableAttributes.length > 0 ? availableAttributes[0].name : undefined, value: "", }); onChange({ ...value }); }; // Remove a condition const handleRemoveCondition = (path: number[]) => { if (path.length === 0) return; const parentPath = path.slice(0, -1); const index = path[path.length - 1]; let currentCondition = value.filter; // Navigate to the parent condition for (const idx of parentPath) { if (!currentCondition.conditions) return; currentCondition = currentCondition.conditions[idx]; } // Remove the condition if (currentCondition.conditions) { currentCondition.conditions = currentCondition.conditions.filter((_, i) => i !== index); onChange({ ...value }); } }; // UI component for rendering a single condition const ConditionItem = ({ condition, path = [] }: { condition: LdapCondition; path: number[]; }) => { const isLogical = logicalOperators.includes(condition.operator); return (
{!isLogical && ( <> {condition.operator !== LdapOperator.PRESENT && ( { const updatedCondition = { ...condition, value: e.target.value }; const newFilter = handleConditionChange(updatedCondition, path); onChange({ ...value, filter: newFilter }); }} /> )} )} {path.length > 0 && ( )}
{isLogical && (
{condition.conditions?.map((childCondition, index) => ( ))}
)}
); }; return ( LDAP Query Builder Build LDAP queries with a visual interface
Visual Builder LDAP Filter Preview
                    {JSON.stringify(value.filter, null, 2)}
                  
{availableAttributes.length > 0 && (
{availableAttributes.map((attr) => ( {attr.name} {attr.type ? `(${attr.type})` : ''} ))}
)}
{onTest && ( )} {onSave && ( )}
); }