Enhance LDAP query builder with drag-and-drop functionality for filter groups.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/734add71-6306-4281-af3d-a32d5349dd4c.jpg
This commit is contained in:
alphaeusmote
2025-04-09 03:26:53 +00:00
parent ecf4f13d11
commit 39080cea83
4 changed files with 2061 additions and 527 deletions
+433 -320
View File
@@ -10,8 +10,27 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { useQuery, useMutation } from "@tanstack/react-query"; import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient"; import { queryClient, apiRequest } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { Loader2, Save, Trash, History, ArrowLeftRight, RefreshCw, Play, Plus, FolderPlus, X } from "lucide-react"; import { Loader2, Save, Trash, History, ArrowLeftRight, RefreshCw, Play, Plus, FolderPlus, X, Copy, GripVertical } from "lucide-react";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
// Drag and drop imports
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -61,6 +80,238 @@ type LdapFilterRevision = {
modifiedAt: string; modifiedAt: string;
}; };
// Sortable Group component for drag-and-drop
interface SortableGroupProps {
id: string;
group: any;
groupIndex: number;
filterBuilder: any;
setFilterBuilder: (value: any) => void;
ldapAttributes: string[];
isLoadingAttributes: boolean;
}
const SortableGroup = ({
id,
group,
groupIndex,
filterBuilder,
setFilterBuilder,
ldapAttributes,
isLoadingAttributes
}: SortableGroupProps) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 1 : 0,
opacity: isDragging ? 0.5 : 1
};
return (
<div
ref={setNodeRef}
style={style}
className="border rounded-md p-4 mb-4 bg-background"
>
<div className="flex justify-between items-center mb-3">
<div className="flex items-center">
<div
className="cursor-grab mr-2 p-1 hover:bg-muted rounded"
{...attributes}
{...listeners}
>
<GripVertical className="h-4 w-4 text-muted-foreground" />
</div>
<Label className="mr-2">Group Operator:</Label>
<Select
value={group.operator}
onValueChange={(value) => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].operator = value;
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
>
<SelectTrigger className="w-24">
<SelectValue placeholder="Operator" />
</SelectTrigger>
<SelectContent>
<SelectItem value="and">AND</SelectItem>
<SelectItem value="or">OR</SelectItem>
</SelectContent>
</Select>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups.splice(groupIndex, 1);
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="space-y-3">
{group.conditions.map((condition: any, condIndex: number) => (
<div key={condIndex} className="grid grid-cols-12 gap-2 items-center">
<div className="col-span-4">
{ldapAttributes.length > 0 ? (
<Select
value={condition.attribute}
onValueChange={(value) => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions[condIndex].attribute = value;
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
>
<SelectTrigger>
<SelectValue placeholder={isLoadingAttributes ? "Loading attributes..." : "Select attribute"} />
</SelectTrigger>
<SelectContent className="max-h-[280px]">
{ldapAttributes.map((attr) => (
<SelectItem key={attr} value={attr}>{attr}</SelectItem>
))}
<SelectItem value="custom">Custom attribute...</SelectItem>
</SelectContent>
</Select>
) : (
<Input
placeholder={isLoadingAttributes ? "Loading attributes..." : "Attribute name"}
value={condition.attribute}
onChange={(e) => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions[condIndex].attribute = e.target.value;
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
/>
)}
{condition.attribute === "custom" && (
<Input
placeholder="Custom attribute name"
className="mt-1"
value=""
onChange={(e) => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions[condIndex].attribute = e.target.value;
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
/>
)}
</div>
<div className="col-span-3">
<Select
value={condition.operator}
onValueChange={(value) => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions[condIndex].operator = value;
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
>
<SelectTrigger>
<SelectValue placeholder="Operator" />
</SelectTrigger>
<SelectContent>
<SelectItem value="equals">Equals</SelectItem>
<SelectItem value="contains">Contains</SelectItem>
<SelectItem value="startsWith">Starts With</SelectItem>
<SelectItem value="endsWith">Ends With</SelectItem>
<SelectItem value="present">Is Present</SelectItem>
<SelectItem value="notEquals">Not Equals</SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-4">
<Input
placeholder="Value"
value={condition.value}
disabled={condition.operator === 'present'}
onChange={(e) => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions[condIndex].value = e.target.value;
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
/>
</div>
<div className="col-span-1 flex justify-end">
<Button
variant="ghost"
size="icon"
onClick={() => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions.splice(condIndex, 1);
if (updatedGroups[groupIndex].conditions.length === 0) {
updatedGroups.splice(groupIndex, 1);
}
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
disabled={group.conditions.length <= 1}
>
<Trash className="h-4 w-4" />
</Button>
</div>
</div>
))}
<Button
variant="outline"
size="sm"
onClick={() => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions.push({
attribute: "",
operator: "equals",
value: ""
});
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
>
<Plus className="h-4 w-4 mr-2" />
Add Condition
</Button>
</div>
</div>
);
};
const LdapQueryBuilderPage = () => { const LdapQueryBuilderPage = () => {
const { user } = useAuth(); const { user } = useAuth();
const { toast } = useToast(); const { toast } = useToast();
@@ -82,6 +333,48 @@ const LdapQueryBuilderPage = () => {
groups: [] groups: []
}); });
// Set up sensors for drag and drop
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
// Handle drag end event for group reordering
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
const activeId = active.id.toString();
const overId = over.id.toString();
const activeIndex = filterBuilder.groups.findIndex(
(_: any, i: number) => `group-${i}` === activeId
);
const overIndex = filterBuilder.groups.findIndex(
(_: any, i: number) => `group-${i}` === overId
);
if (activeIndex !== -1 && overIndex !== -1) {
const newGroups = arrayMove(
filterBuilder.groups,
activeIndex,
overIndex
);
setFilterBuilder({
...filterBuilder,
groups: newGroups
});
}
}
};
// Query to get LDAP connections // Query to get LDAP connections
const { const {
data: connections = [], data: connections = [],
@@ -263,9 +556,7 @@ const LdapQueryBuilderPage = () => {
: filter.filter; : filter.filter;
// Make sure it has valid structure // Make sure it has valid structure
if (filterData && if (filterData && filterData.operator) {
filterData.operator &&
Array.isArray(filterData.conditions)) {
setFilterBuilder(filterData); setFilterBuilder(filterData);
} }
} catch (err) { } catch (err) {
@@ -483,59 +774,42 @@ const LdapQueryBuilderPage = () => {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Date</TableHead> <TableHead>Version</TableHead>
<TableHead>Modified By</TableHead> <TableHead>Modified By</TableHead>
<TableHead>Date</TableHead>
<TableHead>LDAP Filter</TableHead> <TableHead>LDAP Filter</TableHead>
<TableHead className="text-right">Actions</TableHead> <TableHead></TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{revisions && revisions.length > 0 ? ( {revisions.map((rev: LdapFilterRevision) => (
revisions.map((rev: LdapFilterRevision) => (
<TableRow key={rev.id}> <TableRow key={rev.id}>
<TableCell>v{rev.id}</TableCell>
<TableCell>User #{rev.modifiedBy}</TableCell>
<TableCell>{new Date(rev.modifiedAt).toLocaleString()}</TableCell>
<TableCell className="max-w-[300px] truncate">{rev.ldapFilter}</TableCell>
<TableCell> <TableCell>
{new Date(rev.modifiedAt).toLocaleString()}
</TableCell>
<TableCell>User ID: {rev.modifiedBy}</TableCell>
<TableCell>
<code className="text-xs max-w-[300px] block overflow-hidden text-ellipsis">
{rev.ldapFilter}
</code>
</TableCell>
<TableCell className="text-right">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => revertToRevisionMutation.mutate(rev.id)} onClick={() => revertToRevisionMutation.mutate(rev.id)}
disabled={revertToRevisionMutation.isPending} disabled={revertToRevisionMutation.isPending}
> >
{revertToRevisionMutation.isPending ? ( {revertToRevisionMutation.isPending ?
<Loader2 className="h-4 w-4 animate-spin mr-2" /> <Loader2 className="h-4 w-4 animate-spin" /> :
) : ( 'Restore'
<RefreshCw className="h-4 w-4 mr-2" /> }
)}
Revert
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))}
) : (
<TableRow>
<TableCell colSpan={4} className="text-center">
No revision history found
</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
)} )}
<DialogFooter> <DialogFooter>
<Button <Button variant="outline" onClick={() => setShowRevisionsDialog(false)}>
variant="outline"
onClick={() => setShowRevisionsDialog(false)}
>
Close Close
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -548,49 +822,44 @@ const LdapQueryBuilderPage = () => {
const renderTestResultsDialog = () => { const renderTestResultsDialog = () => {
return ( return (
<Dialog open={showTestResultsDialog} onOpenChange={setShowTestResultsDialog}> <Dialog open={showTestResultsDialog} onOpenChange={setShowTestResultsDialog}>
<DialogContent className="max-w-4xl"> <DialogContent className="max-w-3xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Test Results</DialogTitle> <DialogTitle>Test Results</DialogTitle>
<DialogDescription> <DialogDescription>
Results for LDAP filter: <code>{filterQuery}</code> LDAP filter test results for "{filterQuery}"
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="max-h-[60vh] overflow-y-auto"> <div className="max-h-[60vh] overflow-y-auto">
{testResults.length > 0 ? ( {testResults.length > 0 ? (
<div>
<p className="mb-2 text-muted-foreground">Found {testResults.length} results:</p>
<Table> <Table>
<TableCaption>Found {testResults.length} results</TableCaption>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
{Object.keys(testResults[0]).map((key) => ( <TableHead>Distinguished Name</TableHead>
<TableHead key={key}>{key}</TableHead> <TableHead>Name</TableHead>
))}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{testResults.map((result, index) => ( {testResults.map((result: any, index: number) => (
<TableRow key={index}> <TableRow key={index}>
{Object.entries(result).map(([key, value]) => ( <TableCell className="font-mono text-xs break-all">{result.dn}</TableCell>
<TableCell key={key}> <TableCell>{result.cn || result.name || result.displayName || result.sAMAccountName}</TableCell>
{typeof value === 'object'
? JSON.stringify(value)
: String(value)}
</TableCell>
))}
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</div>
) : ( ) : (
<p className="text-center py-8">No results found for this filter</p> <div className="p-8 text-center">
<p>No results found matching this filter.</p>
</div>
)} )}
</div> </div>
<DialogFooter> <DialogFooter>
<Button <Button variant="outline" onClick={() => setShowTestResultsDialog(false)}>
variant="outline"
onClick={() => setShowTestResultsDialog(false)}
>
Close Close
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -600,135 +869,128 @@ const LdapQueryBuilderPage = () => {
}; };
return ( return (
<DashboardLayout title="LDAP Query Builder"> <DashboardLayout>
<div className="container mx-auto py-6"> <div className="container mx-auto py-6">
<div className="flex justify-between items-center mb-6"> <div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
<div> {/* Sidebar */}
<h1 className="text-3xl font-bold tracking-tight">LDAP Query Builder</h1> <Card className="lg:col-span-1">
<p className="text-muted-foreground">
Create, test and manage LDAP queries for Active Directory
</p>
</div>
</div>
<div className="grid md:grid-cols-3 gap-6">
{/* Left Side - Filters List */}
<div className="md:col-span-1">
<Card>
<CardHeader> <CardHeader>
<CardTitle>LDAP Filters</CardTitle> <CardTitle>LDAP Filters</CardTitle>
<CardDescription>Select a connection to view filters</CardDescription> <CardDescription>Saved LDAP query filters</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<Label htmlFor="connection">LDAP Connection</Label> <Label className="mb-2 block">Connection</Label>
<div className="mt-1">
{renderConnectionsDropdown()} {renderConnectionsDropdown()}
</div> </div>
</div>
<div>
<div className="flex justify-between items-center">
<h3 className="font-medium">Saved Filters</h3>
<Button
variant="ghost"
size="sm"
className="h-7 px-2"
onClick={resetForm}
>
New Filter
</Button>
</div>
{renderFiltersList()} {renderFiltersList()}
</div> </div>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div>
{/* Right Side - Filter Editor */} {/* Main content */}
<div className="md:col-span-2"> <Card className="lg:col-span-4">
<Card>
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
{selectedFilter ? `Edit Filter: ${selectedFilter.name}` : 'Create New Filter'} {selectedFilter ? `Edit: ${selectedFilter.name}` : "Create New LDAP Filter"}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
{selectedFilter Build and test LDAP filters for your Active Directory queries
? 'Update your LDAP filter and test it against your Active Directory'
: 'Create a new LDAP filter and test it against your Active Directory'}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="col-span-3"> <div>
<Label htmlFor="filterName">Filter Name</Label> <Label htmlFor="filterName">Filter Name</Label>
<Input <Input
id="filterName" id="filterName"
value={filterName} value={filterName}
onChange={(e) => setFilterName(e.target.value)} onChange={(e) => setFilterName(e.target.value)}
placeholder="Enter a name for your filter" placeholder="My LDAP Filter"
/> />
</div> </div>
<div> <div>
<Label htmlFor="objectClass">Object Class</Label> <Label htmlFor="objectClass">Object Class</Label>
<Select value={objectClass} onValueChange={setObjectClass}> <Select value={objectClass} onValueChange={setObjectClass}>
<SelectTrigger id="objectClass"> <SelectTrigger>
<SelectValue placeholder="Select object type" /> <SelectValue placeholder="Select object class" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="user">User</SelectItem> <SelectItem value="user">User</SelectItem>
<SelectItem value="group">Group</SelectItem> <SelectItem value="group">Group</SelectItem>
<SelectItem value="organizationalUnit">OU</SelectItem>
<SelectItem value="computer">Computer</SelectItem> <SelectItem value="computer">Computer</SelectItem>
<SelectItem value="domain">Domain</SelectItem> <SelectItem value="organizationalUnit">Organizational Unit</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div> </div>
<div> <div>
<Label htmlFor="filterDescription">Description (Optional)</Label> <Label htmlFor="filterDescription">Description</Label>
<Input <Textarea
id="filterDescription" id="filterDescription"
value={filterDescription} value={filterDescription}
onChange={(e) => setFilterDescription(e.target.value)} onChange={(e) => setFilterDescription(e.target.value)}
placeholder="Enter a description" placeholder="What does this filter do?"
rows={2}
/> />
</div> </div>
<Tabs defaultValue="manual" className="w-full"> <Tabs defaultValue="builder">
<TabsList className="grid w-full grid-cols-2"> <TabsList>
<TabsTrigger value="manual">Manual LDAP Query</TabsTrigger> <TabsTrigger value="builder">Visual Builder</TabsTrigger>
<TabsTrigger value="builder"> <TabsTrigger value="raw">Manual LDAP Query</TabsTrigger>
Visual Query Builder
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="manual" className="py-4">
<div className="space-y-2"> <TabsContent value="raw">
<Label htmlFor="ldapQuery">LDAP Query</Label> <div className="space-y-4">
<div>
<Label htmlFor="ldapFilter">LDAP Filter Query</Label>
<Textarea <Textarea
id="ldapQuery" id="ldapFilter"
value={filterQuery} value={filterQuery}
onChange={(e) => setFilterQuery(e.target.value)} onChange={(e) => setFilterQuery(e.target.value)}
placeholder="Enter your LDAP query string (e.g. (objectClass=user)(sAMAccountName=*))" placeholder="(&(objectClass=user)(memberOf=CN=MyGroup,OU=Groups,DC=example,DC=com))"
className="font-mono h-32" rows={5}
className="font-mono"
/> />
</div> </div>
<div className="flex space-x-2">
<Button
variant="secondary"
size="sm"
onClick={() => testFilterMutation.mutate()}
disabled={!filterQuery || testFilterMutation.isPending}
>
{testFilterMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<Play className="h-4 w-4 mr-2" />
)}
Test Query
</Button>
</div>
</div>
</TabsContent> </TabsContent>
<TabsContent value="builder" className="py-4">
<TabsContent value="builder">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center"> {/* Main operator selection */}
<Label>Query Conditions</Label> <div>
<div className="space-x-2"> <Label className="mb-2">Main Operator</Label>
<div className="flex items-center">
<Select <Select
value={filterBuilder.operator} value={filterBuilder.operator}
onValueChange={(value) => setFilterBuilder({ onValueChange={(value) => {
setFilterBuilder({
...filterBuilder, ...filterBuilder,
operator: value operator: value
})} });
}}
> >
<SelectTrigger className="w-32"> <SelectTrigger className="w-32">
<SelectValue placeholder="Operator" /> <SelectValue placeholder="Operator" />
@@ -769,186 +1031,29 @@ const LdapQueryBuilderPage = () => {
</div> </div>
{filterBuilder.groups && filterBuilder.groups.length > 0 ? ( {filterBuilder.groups && filterBuilder.groups.length > 0 ? (
filterBuilder.groups.map((group: any, groupIndex: number) => ( <DndContext
<div key={groupIndex} className="border rounded-md p-4 mb-4 bg-background"> sensors={sensors}
<div className="flex justify-between items-center mb-3"> collisionDetection={closestCenter}
<div className="flex items-center"> onDragEnd={handleDragEnd}
<Label className="mr-2">Group Operator:</Label>
<Select
value={group.operator}
onValueChange={(value) => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].operator = value;
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
> >
<SelectTrigger className="w-24"> <SortableContext
<SelectValue placeholder="Operator" /> items={filterBuilder.groups.map((_: any, index: number) => `group-${index}`)}
</SelectTrigger> strategy={verticalListSortingStrategy}
<SelectContent>
<SelectItem value="and">AND</SelectItem>
<SelectItem value="or">OR</SelectItem>
</SelectContent>
</Select>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups.splice(groupIndex, 1);
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
> >
<X className="h-4 w-4" /> {filterBuilder.groups.map((group: any, index: number) => (
</Button> <SortableGroup
</div> key={index}
id={`group-${index}`}
<div className="space-y-3"> group={group}
{group.conditions.map((condition: any, condIndex: number) => ( groupIndex={index}
<div key={condIndex} className="grid grid-cols-12 gap-2 items-center"> filterBuilder={filterBuilder}
<div className="col-span-4"> setFilterBuilder={setFilterBuilder}
{ldapAttributes.length > 0 ? ( ldapAttributes={ldapAttributes}
<Select isLoadingAttributes={isLoadingAttributes}
value={condition.attribute} />
onValueChange={(value) => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions[condIndex].attribute = value;
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
>
<SelectTrigger>
<SelectValue placeholder={isLoadingAttributes ? "Loading attributes..." : "Select attribute"} />
</SelectTrigger>
<SelectContent className="max-h-[280px]">
{ldapAttributes.map((attr) => (
<SelectItem key={attr} value={attr}>{attr}</SelectItem>
))} ))}
<SelectItem value="custom">Custom attribute...</SelectItem> </SortableContext>
</SelectContent> </DndContext>
</Select>
) : (
<Input
placeholder={isLoadingAttributes ? "Loading attributes..." : "Attribute name"}
value={condition.attribute}
onChange={(e) => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions[condIndex].attribute = e.target.value;
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
/>
)}
{condition.attribute === "custom" && (
<Input
placeholder="Custom attribute name"
className="mt-1"
value=""
onChange={(e) => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions[condIndex].attribute = e.target.value;
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
/>
)}
</div>
<div className="col-span-3">
<Select
value={condition.operator}
onValueChange={(value) => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions[condIndex].operator = value;
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
>
<SelectTrigger>
<SelectValue placeholder="Operator" />
</SelectTrigger>
<SelectContent>
<SelectItem value="equals">Equals</SelectItem>
<SelectItem value="contains">Contains</SelectItem>
<SelectItem value="startsWith">Starts With</SelectItem>
<SelectItem value="endsWith">Ends With</SelectItem>
<SelectItem value="present">Is Present</SelectItem>
<SelectItem value="notEquals">Not Equals</SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-4">
<Input
placeholder="Value"
value={condition.value}
disabled={condition.operator === 'present'}
onChange={(e) => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions[condIndex].value = e.target.value;
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
/>
</div>
<div className="col-span-1 flex justify-end">
<Button
variant="ghost"
size="icon"
onClick={() => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions.splice(condIndex, 1);
if (updatedGroups[groupIndex].conditions.length === 0) {
updatedGroups.splice(groupIndex, 1);
}
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
disabled={group.conditions.length <= 1}
>
<Trash className="h-4 w-4" />
</Button>
</div>
</div>
))}
<Button
variant="outline"
size="sm"
onClick={() => {
const updatedGroups = [...filterBuilder.groups];
updatedGroups[groupIndex].conditions.push(
{ attribute: "", operator: "equals", value: "" }
);
setFilterBuilder({
...filterBuilder,
groups: updatedGroups
});
}}
>
<Plus className="h-4 w-4 mr-2" />
Add Condition to Group
</Button>
</div>
</div>
))
) : ( ) : (
<div className="p-8 text-center border rounded-md bg-background"> <div className="p-8 text-center border rounded-md bg-background">
<p className="text-muted-foreground mb-4">No condition groups added yet</p> <p className="text-muted-foreground mb-4">No condition groups added yet</p>
@@ -1001,11 +1106,29 @@ const LdapQueryBuilderPage = () => {
</div> </div>
)} )}
{/* Display LDAP Filter */} {/* Display LDAP Filter with copy functionality */}
{filterQuery && ( {filterQuery && (
<div className="mt-4 p-3 border rounded-md bg-muted"> <div className="mt-4 p-3 border rounded-md bg-muted">
<Label className="text-xs mb-1 block">Generated LDAP Filter:</Label> <div className="flex justify-between items-center mb-1">
<code className="text-xs break-all">{filterQuery}</code> <Label className="text-xs">Generated LDAP Filter:</Label>
<Button
variant="ghost"
size="sm"
className="h-6 px-2 py-0"
onClick={() => {
navigator.clipboard.writeText(filterQuery);
toast({
title: "Copied to clipboard",
description: "LDAP query has been copied to your clipboard",
duration: 2000,
});
}}
>
<Copy className="h-3.5 w-3.5 mr-1" />
<span className="text-xs">Copy</span>
</Button>
</div>
<code className="text-xs break-all block p-2 bg-background rounded border">{filterQuery}</code>
</div> </div>
)} )}
</div> </div>
@@ -1045,11 +1168,12 @@ const LdapQueryBuilderPage = () => {
)} )}
Delete Delete
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
refetchRevisions();
setShowRevisionsDialog(true); setShowRevisionsDialog(true);
refetchRevisions();
}} }}
> >
<History className="h-4 w-4 mr-2" /> <History className="h-4 w-4 mr-2" />
@@ -1058,28 +1182,19 @@ const LdapQueryBuilderPage = () => {
</> </>
)} )}
</div> </div>
<div className="space-x-2"> <div className="space-x-2">
<Button <Button
variant="outline" variant="outline"
onClick={() => testFilterMutation.mutate()} onClick={resetForm}
disabled={!selectedConnectionId || !filterQuery || testFilterMutation.isPending}
> >
{testFilterMutation.isPending ? ( New Filter
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<Play className="h-4 w-4 mr-2" />
)}
Test Query
</Button> </Button>
<Button <Button
variant="default"
onClick={handleSaveFilter} onClick={handleSaveFilter}
disabled={ disabled={createFilterMutation.isPending || updateFilterMutation.isPending}
!selectedConnectionId ||
!filterName ||
!filterQuery ||
createFilterMutation.isPending ||
updateFilterMutation.isPending
}
> >
{(createFilterMutation.isPending || updateFilterMutation.isPending) ? ( {(createFilterMutation.isPending || updateFilterMutation.isPending) ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" /> <Loader2 className="h-4 w-4 animate-spin mr-2" />
@@ -1095,13 +1210,11 @@ const LdapQueryBuilderPage = () => {
</Card> </Card>
</div> </div>
</div> </div>
</div>
{/* Dialogs */}
{renderRevisionsDialog()} {renderRevisionsDialog()}
{renderTestResultsDialog()} {renderTestResultsDialog()}
</DashboardLayout> </DashboardLayout>
); );
} };
export default LdapQueryBuilderPage; export default LdapQueryBuilderPage;
File diff suppressed because it is too large Load Diff
+56
View File
@@ -9,6 +9,9 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@jridgewell/trace-mapping": "^0.3.25", "@jridgewell/trace-mapping": "^0.3.25",
"@neondatabase/serverless": "^0.10.4", "@neondatabase/serverless": "^0.10.4",
@@ -466,6 +469,59 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@drizzle-team/brocli": { "node_modules/@drizzle-team/brocli": {
"version": "0.10.2", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
+3
View File
@@ -11,6 +11,9 @@
"db:push": "drizzle-kit push" "db:push": "drizzle-kit push"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@jridgewell/trace-mapping": "^0.3.25", "@jridgewell/trace-mapping": "^0.3.25",
"@neondatabase/serverless": "^0.10.4", "@neondatabase/serverless": "^0.10.4",