import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, ArrowDown, ArrowUp, ArrowUpDown, Check, Columns, Database, GripVertical, Lock, RefreshCw, Search, Server, Shield, ShieldCheck, Trash2, Unlock, X, } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { dashboardAPI, repositoryAPI } from "../utils/api"; const Repositories = () => { const queryClient = useQueryClient(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [searchTerm, setSearchTerm] = useState(""); const [filterType, setFilterType] = useState("all"); // all, secure, insecure const [filterStatus, setFilterStatus] = useState("all"); // all, active, inactive const [hostFilter, setHostFilter] = useState(""); const [sortField, setSortField] = useState("name"); const [sortDirection, setSortDirection] = useState("asc"); const [showColumnSettings, setShowColumnSettings] = useState(false); const [deleteModalData, setDeleteModalData] = useState(null); // Handle host filter from URL parameter useEffect(() => { const hostParam = searchParams.get("host"); if (hostParam) { setHostFilter(hostParam); } }, [searchParams]); // Column configuration const [columnConfig, setColumnConfig] = useState(() => { const defaultConfig = [ { id: "name", label: "Repository", visible: true, order: 0 }, { id: "url", label: "URL", visible: true, order: 1 }, { id: "distribution", label: "Distribution", visible: true, order: 2 }, { id: "security", label: "Security", visible: true, order: 3 }, { id: "status", label: "Status", visible: true, order: 4 }, { id: "hostCount", label: "Hosts", visible: true, order: 5 }, { id: "actions", label: "Actions", visible: true, order: 6 }, ]; const saved = localStorage.getItem("repositories-column-config"); if (saved) { try { return JSON.parse(saved); } catch (e) { console.error("Failed to parse saved column config:", e); } } return defaultConfig; }); const updateColumnConfig = (newConfig) => { setColumnConfig(newConfig); localStorage.setItem( "repositories-column-config", JSON.stringify(newConfig), ); }; // Fetch repositories const { data: repositories = [], isLoading, error, refetch, isFetching, } = useQuery({ queryKey: ["repositories"], queryFn: () => repositoryAPI.list().then((res) => res.data), }); // Fetch repository statistics const { data: stats } = useQuery({ queryKey: ["repository-stats"], queryFn: () => repositoryAPI.getStats().then((res) => res.data), }); // Fetch host information when filtering by host const { data: hosts } = useQuery({ queryKey: ["hosts"], queryFn: () => dashboardAPI.getHosts().then((res) => res.data), staleTime: 5 * 60 * 1000, enabled: !!hostFilter, }); // Get the filtered host information const filteredHost = hosts?.find((host) => host.id === hostFilter); // Delete repository mutation const deleteRepositoryMutation = useMutation({ mutationFn: (repositoryId) => repositoryAPI.delete(repositoryId), onSuccess: () => { queryClient.invalidateQueries(["repositories"]); queryClient.invalidateQueries(["repository-stats"]); }, }); // Get visible columns in order const visibleColumns = columnConfig .filter((col) => col.visible) .sort((a, b) => a.order - b.order); // Sorting functions const handleSort = (field) => { if (sortField === field) { setSortDirection(sortDirection === "asc" ? "desc" : "asc"); } else { setSortField(field); setSortDirection("asc"); } }; const getSortIcon = (field) => { if (sortField !== field) return ; return sortDirection === "asc" ? ( ) : ( ); }; // Column management functions const toggleColumnVisibility = (columnId) => { const newConfig = columnConfig.map((col) => col.id === columnId ? { ...col, visible: !col.visible } : col, ); updateColumnConfig(newConfig); }; const reorderColumns = (fromIndex, toIndex) => { const newConfig = [...columnConfig]; const [movedColumn] = newConfig.splice(fromIndex, 1); newConfig.splice(toIndex, 0, movedColumn); // Update order values const updatedConfig = newConfig.map((col, index) => ({ ...col, order: index, })); updateColumnConfig(updatedConfig); }; const resetColumns = () => { const defaultConfig = [ { id: "name", label: "Repository", visible: true, order: 0 }, { id: "url", label: "URL", visible: true, order: 1 }, { id: "distribution", label: "Distribution", visible: true, order: 2 }, { id: "security", label: "Security", visible: true, order: 3 }, { id: "status", label: "Status", visible: true, order: 4 }, { id: "hostCount", label: "Hosts", visible: true, order: 5 }, { id: "actions", label: "Actions", visible: true, order: 6 }, ]; updateColumnConfig(defaultConfig); }; const handleDeleteRepository = (repo, e) => { e.preventDefault(); e.stopPropagation(); setDeleteModalData({ id: repo.id, name: repo.name, hostCount: repo.hostCount || 0, }); }; const handleRowClick = (repo) => { navigate(`/repositories/${repo.id}`); }; const confirmDelete = () => { if (deleteModalData) { deleteRepositoryMutation.mutate(deleteModalData.id); setDeleteModalData(null); } }; const cancelDelete = () => { setDeleteModalData(null); }; // Filter and sort repositories const filteredAndSortedRepositories = useMemo(() => { if (!repositories) return []; // Filter repositories const filtered = repositories.filter((repo) => { const matchesSearch = repo.name.toLowerCase().includes(searchTerm.toLowerCase()) || repo.url.toLowerCase().includes(searchTerm.toLowerCase()) || repo.distribution.toLowerCase().includes(searchTerm.toLowerCase()); // Check security based on URL if isSecure property doesn't exist const isSecure = repo.isSecure !== undefined ? repo.isSecure : repo.url.startsWith("https://"); const matchesType = filterType === "all" || (filterType === "secure" && isSecure) || (filterType === "insecure" && !isSecure); const matchesStatus = filterStatus === "all" || (filterStatus === "active" && repo.is_active === true) || (filterStatus === "inactive" && repo.is_active === false); // Filter by host if hostFilter is set const matchesHost = !hostFilter || repo.hosts?.some((host) => host.id === hostFilter); return matchesSearch && matchesType && matchesStatus && matchesHost; }); // Sort repositories const sorted = filtered.sort((a, b) => { let aValue = a[sortField]; let bValue = b[sortField]; // Handle special cases if (sortField === "security") { // Use the same logic as filtering to determine isSecure const aIsSecure = a.isSecure !== undefined ? a.isSecure : a.url.startsWith("https://"); const bIsSecure = b.isSecure !== undefined ? b.isSecure : b.url.startsWith("https://"); // Sort by boolean: true (Secure) comes before false (Insecure) when ascending aValue = aIsSecure ? 1 : 0; bValue = bIsSecure ? 1 : 0; } else if (sortField === "status") { aValue = a.is_active ? "Active" : "Inactive"; bValue = b.is_active ? "Active" : "Inactive"; } if (typeof aValue === "string") { aValue = aValue.toLowerCase(); bValue = bValue.toLowerCase(); } if (aValue < bValue) return sortDirection === "asc" ? -1 : 1; if (aValue > bValue) return sortDirection === "asc" ? 1 : -1; return 0; }); return sorted; }, [ repositories, searchTerm, filterType, filterStatus, sortField, sortDirection, hostFilter, ]); if (isLoading) { return (
); } if (error) { return (
Failed to load repositories: {error.message}
); } return (
{/* Delete Confirmation Modal */} {deleteModalData && (

Delete Repository

Are you sure you want to delete{" "} "{deleteModalData.name}"?

{deleteModalData.hostCount > 0 && (

⚠️ This repository is currently assigned to{" "} {deleteModalData.hostCount} host {deleteModalData.hostCount !== 1 ? "s" : ""}.

)}

This action cannot be undone.

)} {/* Page Header */}

Repositories

Manage and monitor your package repositories

{/* Summary Stats */}

Total Repositories

{stats?.totalRepositories || 0}

Active Repositories

{stats?.activeRepositories || 0}

Secure (HTTPS)

{stats?.secureRepositories || 0}

Security Score

{stats?.securityPercentage || 0}%

{/* Repositories List */}
{/* Empty selection controls area to match packages page spacing */}
{/* Table Controls */}
{/* Search */}
setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white placeholder-secondary-500 dark:placeholder-secondary-400" />
{/* Host Filter Indicator */} {hostFilter && filteredHost && (
Filtered by: {filteredHost.friendly_name}
)} {/* Security Filter */}
{/* Status Filter */}
{/* Columns Button */}
{filteredAndSortedRepositories.length === 0 ? (

{repositories?.length === 0 ? "No repositories found" : "No repositories match your filters"}

{repositories?.length === 0 && (

No repositories have been reported by your hosts yet

)}
) : (
{visibleColumns.map((column) => ( ))} {filteredAndSortedRepositories.map((repo) => ( handleRowClick(repo)} > {visibleColumns.map((column) => ( ))} ))}
{renderCellContent(column, repo)}
)}
{/* Column Settings Modal */} {showColumnSettings && ( setShowColumnSettings(false)} onToggleVisibility={toggleColumnVisibility} onReorder={reorderColumns} onReset={resetColumns} /> )}
); // Render cell content based on column type function renderCellContent(column, repo) { switch (column.id) { case "name": return (
{repo.name}
); case "url": return (
{repo.url}
); case "distribution": return (
{repo.distribution}
); case "security": { const isSecure = repo.isSecure !== undefined ? repo.isSecure : repo.url.startsWith("https://"); return (
{isSecure ? (
Secure
) : (
Insecure
)}
); } case "status": return ( {repo.is_active ? "Active" : "Inactive"} ); case "hostCount": return (
{repo.hostCount}
); case "actions": return (
); default: return null; } } }; // Column Settings Modal Component const ColumnSettingsModal = ({ columnConfig, onClose, onToggleVisibility, onReorder, onReset, }) => { const [draggedIndex, setDraggedIndex] = useState(null); const handleDragStart = (e, index) => { setDraggedIndex(index); e.dataTransfer.effectAllowed = "move"; }; const handleDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; }; const handleDrop = (e, dropIndex) => { e.preventDefault(); if (draggedIndex !== null && draggedIndex !== dropIndex) { onReorder(draggedIndex, dropIndex); } setDraggedIndex(null); }; return (

Column Settings

{columnConfig.map((column, index) => ( ))}
); }; export default Repositories;