mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-31 04:58:05 +00:00
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:
@@ -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
Generated
+56
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user