import { useState, useEffect } from "react"; import { useToast } from "@/hooks/use-toast"; import { useQuery, useMutation } from "@tanstack/react-query"; import { queryClient } from "@/lib/queryClient"; import { useLocation } from "wouter"; import { Button } from "@/components/ui/button"; import { Plus, Edit, Trash2, RefreshCw, HardDrive, Network, ChevronLeft } from "lucide-react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter, DialogClose, } from "@/components/ui/dialog"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; interface AdSite { id: number; objectGUID: string; name: string; dn: string; canonicalName: string; description: string; location: string; subnets: string[] | null; managedBy: string | null; objectType: string; connectionId: number; adProperties: Record; } interface AdSubnet { id: number; objectGUID: string; name: string; dn: string; canonicalName: string; description: string; location: string; siteObject: string | null; cidr: string | null; managedBy: string | null; objectType: string; connectionId: number; adProperties: Record; } const siteSchema = z.object({ name: z.string().min(1, "Name is required"), description: z.string().optional(), location: z.string().optional(), }); const subnetSchema = z.object({ name: z.string().min(1, "Name is required"), description: z.string().optional(), location: z.string().optional(), siteObject: z.string().optional(), cidr: z.string().optional(), }); type SiteFormValues = z.infer; type SubnetFormValues = z.infer; const MAX_ITEMS_PER_PAGE = 10; export default function SitesPage() { const { toast } = useToast(); const [location, setLocation] = useLocation(); const [selectedConnection, setSelectedConnection] = useState(null); const [activeTab, setActiveTab] = useState("sites"); // Pagination state const [currentSitePage, setCurrentSitePage] = useState(1); const [totalSitePages, setTotalSitePages] = useState(1); const [currentSubnetPage, setCurrentSubnetPage] = useState(1); const [totalSubnetPages, setTotalSubnetPages] = useState(1); // Editing state const [editingSite, setEditingSite] = useState(null); const [editingSubnet, setEditingSubnet] = useState(null); // Dialog state const [isSiteFormOpen, setIsSiteFormOpen] = useState(false); const [isSubnetFormOpen, setIsSubnetFormOpen] = useState(false); // Get LDAP connections const { data: connections = [], isLoading: isLoadingConnections } = useQuery({ queryKey: ['/api/ldap-connections'], }); // Set first connection as default when data loads useEffect(() => { if (connections.length > 0 && !selectedConnection) { setSelectedConnection(connections[0].id); } }, [connections, selectedConnection]); interface ApiResponse { data: T[]; metadata: { totalPages: number; currentPage: number; totalRecordCount: number; }; } // Get sites from the selected connection const { data: sitesData, isLoading: isLoadingSites, refetch: refetchSites, error: sitesError } = useQuery>({ queryKey: selectedConnection ? [`/api/connections/${selectedConnection}/ad-sites`, { top: MAX_ITEMS_PER_PAGE, skip: (currentSitePage - 1) * MAX_ITEMS_PER_PAGE }] : [], enabled: !!selectedConnection, }); // Get subnets from the selected connection const { data: subnetsData, isLoading: isLoadingSubnets, refetch: refetchSubnets, error: subnetsError } = useQuery>({ queryKey: selectedConnection ? [`/api/connections/${selectedConnection}/ad-subnets`, { top: MAX_ITEMS_PER_PAGE, skip: (currentSubnetPage - 1) * MAX_ITEMS_PER_PAGE }] : [], enabled: !!selectedConnection, }); // Update pagination information when data changes useEffect(() => { // Debug logging console.log("Sites Data:", sitesData); console.log("Sites Error:", sitesError); console.log("Subnets Data:", subnetsData); console.log("Subnets Error:", subnetsError); if (sitesData?.metadata) { setTotalSitePages(sitesData.metadata.totalPages || 1); } if (subnetsData?.metadata) { setTotalSubnetPages(subnetsData.metadata.totalPages || 1); } }, [sitesData, subnetsData, sitesError, subnetsError]); // Site form setup const siteForm = useForm({ resolver: zodResolver(siteSchema), defaultValues: { name: "", description: "", location: "", }, }); // Subnet form setup const subnetForm = useForm({ resolver: zodResolver(subnetSchema), defaultValues: { name: "", description: "", location: "", siteObject: "", cidr: "", }, }); // Reset and populate forms when editing useEffect(() => { if (editingSite) { siteForm.reset({ name: editingSite.name, description: editingSite.description || "", location: editingSite.location || "", }); setIsSiteFormOpen(true); } else { siteForm.reset({ name: "", description: "", location: "", }); } }, [editingSite, siteForm]); useEffect(() => { if (editingSubnet) { subnetForm.reset({ name: editingSubnet.name, description: editingSubnet.description || "", location: editingSubnet.location || "", siteObject: editingSubnet.siteObject || "", cidr: editingSubnet.cidr || "", }); setIsSubnetFormOpen(true); } else { subnetForm.reset({ name: "", description: "", location: "", siteObject: "", cidr: "", }); } }, [editingSubnet, subnetForm]); // Create site mutation const createSiteMutation = useMutation({ mutationFn: async (data: SiteFormValues) => { const response = await fetch(`/api/connections/${selectedConnection}/ad-sites`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to create site'); } return response.json(); }, onSuccess: () => { toast({ title: "Site created", description: "The site has been created successfully.", }); setIsSiteFormOpen(false); queryClient.invalidateQueries({ queryKey: [`/api/connections/${selectedConnection}/ad-sites`] }); }, onError: (error) => { toast({ title: "Error creating site", description: error.message, variant: "destructive", }); }, }); // Update site mutation const updateSiteMutation = useMutation({ mutationFn: async ({ objectGUID, data }: { objectGUID: string; data: SiteFormValues }) => { const response = await fetch(`/api/connections/${selectedConnection}/ad-sites/${objectGUID}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to update site'); } return response.json(); }, onSuccess: () => { toast({ title: "Site updated", description: "The site has been updated successfully.", }); setEditingSite(null); setIsSiteFormOpen(false); queryClient.invalidateQueries({ queryKey: [`/api/connections/${selectedConnection}/ad-sites`] }); }, onError: (error) => { toast({ title: "Error updating site", description: error.message, variant: "destructive", }); }, }); // Delete site mutation const deleteSiteMutation = useMutation({ mutationFn: async (objectGUID: string) => { const response = await fetch(`/api/connections/${selectedConnection}/ad-sites/${objectGUID}`, { method: 'DELETE', }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to delete site'); } return response.json(); }, onSuccess: () => { toast({ title: "Site deleted", description: "The site has been deleted successfully.", }); queryClient.invalidateQueries({ queryKey: [`/api/connections/${selectedConnection}/ad-sites`] }); }, onError: (error) => { toast({ title: "Error deleting site", description: error.message, variant: "destructive", }); }, }); // Create subnet mutation const createSubnetMutation = useMutation({ mutationFn: async (data: SubnetFormValues) => { const response = await fetch(`/api/connections/${selectedConnection}/ad-subnets`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to create subnet'); } return response.json(); }, onSuccess: () => { toast({ title: "Subnet created", description: "The subnet has been created successfully.", }); setIsSubnetFormOpen(false); queryClient.invalidateQueries({ queryKey: [`/api/connections/${selectedConnection}/ad-subnets`] }); }, onError: (error) => { toast({ title: "Error creating subnet", description: error.message, variant: "destructive", }); }, }); // Update subnet mutation const updateSubnetMutation = useMutation({ mutationFn: async ({ objectGUID, data }: { objectGUID: string; data: SubnetFormValues }) => { const response = await fetch(`/api/connections/${selectedConnection}/ad-subnets/${objectGUID}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to update subnet'); } return response.json(); }, onSuccess: () => { toast({ title: "Subnet updated", description: "The subnet has been updated successfully.", }); setEditingSubnet(null); setIsSubnetFormOpen(false); queryClient.invalidateQueries({ queryKey: [`/api/connections/${selectedConnection}/ad-subnets`] }); }, onError: (error) => { toast({ title: "Error updating subnet", description: error.message, variant: "destructive", }); }, }); // Delete subnet mutation const deleteSubnetMutation = useMutation({ mutationFn: async (objectGUID: string) => { const response = await fetch(`/api/connections/${selectedConnection}/ad-subnets/${objectGUID}`, { method: 'DELETE', }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to delete subnet'); } return response.json(); }, onSuccess: () => { toast({ title: "Subnet deleted", description: "The subnet has been deleted successfully.", }); queryClient.invalidateQueries({ queryKey: [`/api/connections/${selectedConnection}/ad-subnets`] }); }, onError: (error) => { toast({ title: "Error deleting subnet", description: error.message, variant: "destructive", }); }, }); // Handle site form submission const onSiteSubmit = (data: SiteFormValues) => { if (editingSite) { updateSiteMutation.mutate({ objectGUID: editingSite.objectGUID, data }); } else { createSiteMutation.mutate(data); } }; // Handle subnet form submission const onSubnetSubmit = (data: SubnetFormValues) => { if (editingSubnet) { updateSubnetMutation.mutate({ objectGUID: editingSubnet.objectGUID, data }); } else { createSubnetMutation.mutate(data); } }; return (

AD Sites and Subnets

Sites Subnets
Active Directory Sites Manage physical locations in your Active Directory infrastructure.
{isLoadingSites || !selectedConnection ? (
{[...Array(5)].map((_, i) => (
))}
) : sitesError ? (

Error loading sites

{sitesError.message || "Unknown error occurred"}

) : sitesData?.data && sitesData.data.length > 0 ? ( Name Description Location Associated Subnets Actions {sitesData?.data?.map((site: AdSite) => ( {site.name} {site.description || '—'} {site.location || '—'} {site.subnets && site.subnets.length > 0 ? site.subnets.map((subnet, idx) => ( {subnet.split(',')[0].replace('CN=', '')} )) : '—'}
Are you sure you want to delete this site? This action cannot be undone. This will permanently delete the site "{site.name}" and all of its data from the server. Cancel deleteSiteMutation.mutate(site.objectGUID)} className="bg-destructive text-destructive-foreground" > Delete
))}
) : (

No sites found for this connection.

)} {/* Pagination for sites */} {sitesData?.data && sitesData.data.length > 0 && totalSitePages > 1 && (
{Array.from({ length: totalSitePages }, (_, i) => i + 1).map((page) => ( ))}
)}
{/* Site Form Dialog */} {editingSite ? 'Edit Site' : 'Create New Site'}
( Name* The name of the Active Directory site. )} /> ( Description