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 && (
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 */}
{/* 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 (
);
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) => (
))}
Reset to Default
Done
);
};
export default Repositories;