import { useQuery } from "@tanstack/react-query"; import { AlertTriangle, ArrowDown, ArrowUp, ArrowUpDown, Check, Columns, Database, Eye, Globe, GripVertical, Lock, RefreshCw, Search, Server, Shield, ShieldCheck, Unlock, Users, X, } from "lucide-react"; import React, { useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { repositoryAPI } from "../utils/api"; const Repositories = () => { const [searchTerm, setSearchTerm] = useState(""); const [filterType, setFilterType] = useState("all"); // all, secure, insecure const [filterStatus, setFilterStatus] = useState("all"); // all, active, inactive const [sortField, setSortField] = useState("name"); const [sortDirection, setSortDirection] = useState("asc"); const [showColumnSettings, setShowColumnSettings] = useState(false); // 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), }); // 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); }; // 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); return matchesSearch && matchesType && matchesStatus; }); // Sort repositories const sorted = filtered.sort((a, b) => { let aValue = a[sortField]; let bValue = b[sortField]; // Handle special cases if (sortField === "security") { aValue = a.isSecure ? "Secure" : "Insecure"; bValue = b.isSecure ? "Secure" : "Insecure"; } 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, ]); if (isLoading) { return (
); } if (error) { return (
Failed to load repositories: {error.message}
); } return (
{/* 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" />
{/* 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) => ( {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.host_count}
); case "actions": return ( View ); 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) => (
handleDragStart(e, index)} onDragOver={handleDragOver} onDrop={(e) => handleDrop(e, index)} className="flex items-center justify-between p-3 bg-secondary-50 dark:bg-secondary-700 rounded-lg cursor-move hover:bg-secondary-100 dark:hover:bg-secondary-600 transition-colors" >
{column.label}
))}
); }; export default Repositories;