improved table views and added more host information

This commit is contained in:
Muhammad Ibrahim
2025-09-20 10:56:59 +01:00
parent 216c9dbefa
commit adb207fef9
43 changed files with 6376 additions and 684 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import {
@@ -11,7 +11,15 @@ import {
Lock,
Unlock,
Database,
Eye
Eye,
Search,
Columns,
ArrowUpDown,
ArrowUp,
ArrowDown,
X,
GripVertical,
Check
} from 'lucide-react';
import { repositoryAPI } from '../utils/api';
@@ -19,6 +27,37 @@ 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 } = useQuery({
@@ -32,22 +71,122 @@ const Repositories = () => {
queryFn: () => repositoryAPI.getStats().then(res => res.data)
});
// Filter repositories based on search and filters
const filteredRepositories = repositories.filter(repo => {
const matchesSearch = repo.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
repo.url.toLowerCase().includes(searchTerm.toLowerCase()) ||
repo.distribution.toLowerCase().includes(searchTerm.toLowerCase());
// 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 <ArrowUpDown className="h-4 w-4" />
return sortDirection === 'asc' ? <ArrowUp className="h-4 w-4" /> : <ArrowDown className="h-4 w-4" />
};
// 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)
const matchesType = filterType === 'all' ||
(filterType === 'secure' && repo.isSecure) ||
(filterType === 'insecure' && !repo.isSecure);
// 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 []
const matchesStatus = filterStatus === 'all' ||
(filterStatus === 'active' && repo.isActive) ||
(filterStatus === 'inactive' && !repo.isActive);
return matchesSearch && matchesType && matchesStatus;
});
// 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());
// Debug logging
console.log('Filtering repo:', {
name: repo.name,
isSecure: repo.isSecure,
filterType,
url: repo.url
});
// 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.isActive === true) ||
(filterStatus === 'inactive' && repo.isActive === false);
console.log('Filter results:', {
matchesSearch,
matchesType,
matchesStatus,
final: matchesSearch && matchesType && matchesStatus
});
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.isActive ? 'Active' : 'Inactive';
bValue = b.isActive ? '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 (
@@ -71,202 +210,331 @@ const Repositories = () => {
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-secondary-900 dark:text-white">
Repositories
</h1>
<p className="text-secondary-500 dark:text-secondary-300 mt-1">
Manage and monitor package repositories across your infrastructure
</p>
</div>
</div>
<div className="h-[calc(100vh-7rem)] flex flex-col overflow-hidden">
{/* Statistics Cards */}
{stats && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow hover:shadow-md transition-shadow p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<Database className="h-8 w-8 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-secondary-500 dark:text-secondary-300">Total Repositories</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">{stats.totalRepositories}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow hover:shadow-md transition-shadow p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<Server className="h-8 w-8 text-green-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-secondary-500 dark:text-secondary-300">Active Repositories</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">{stats.activeRepositories}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow hover:shadow-md transition-shadow p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<Shield className="h-8 w-8 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-secondary-500 dark:text-secondary-300">Secure (HTTPS)</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">{stats.secureRepositories}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow hover:shadow-md transition-shadow p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="relative">
<ShieldCheck className="h-8 w-8 text-green-600" />
<span className="absolute -top-1 -right-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 text-xs font-medium px-1.5 py-0.5 rounded-full">
{stats.securityPercentage}%
</span>
</div>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-secondary-500 dark:text-secondary-300">Security Score</p>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">{stats.securityPercentage}%</p>
</div>
{/* Summary Stats */}
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mb-6 flex-shrink-0">
<div className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200">
<div className="flex items-center">
<Database className="h-5 w-5 text-primary-600 mr-2" />
<div>
<p className="text-sm text-secondary-500 dark:text-white">Total Repositories</p>
<p className="text-xl font-semibold text-secondary-900 dark:text-white">{stats?.totalRepositories || 0}</p>
</div>
</div>
</div>
)}
{/* Search and Filters */}
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow p-6">
<div className="flex flex-col sm:flex-row gap-4">
{/* Search */}
<div className="flex-1">
<input
type="text"
placeholder="Search repositories..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-secondary-700 dark:text-white"
/>
<div className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200">
<div className="flex items-center">
<Server className="h-5 w-5 text-success-600 mr-2" />
<div>
<p className="text-sm text-secondary-500 dark:text-white">Active Repositories</p>
<p className="text-xl font-semibold text-secondary-900 dark:text-white">{stats?.activeRepositories || 0}</p>
</div>
</div>
{/* Security Filter */}
<div>
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
className="px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-secondary-700 dark:text-white"
>
<option value="all">All Security Types</option>
<option value="secure">HTTPS Only</option>
<option value="insecure">HTTP Only</option>
</select>
</div>
<div className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200">
<div className="flex items-center">
<Shield className="h-5 w-5 text-warning-600 mr-2" />
<div>
<p className="text-sm text-secondary-500 dark:text-white">Secure (HTTPS)</p>
<p className="text-xl font-semibold text-secondary-900 dark:text-white">{stats?.secureRepositories || 0}</p>
</div>
</div>
{/* Status Filter */}
<div>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-secondary-700 dark:text-white"
>
<option value="all">All Statuses</option>
<option value="active">Active Only</option>
<option value="inactive">Inactive Only</option>
</select>
</div>
<div className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200">
<div className="flex items-center">
<ShieldCheck className="h-5 w-5 text-danger-600 mr-2" />
<div>
<p className="text-sm text-secondary-500 dark:text-white">Security Score</p>
<p className="text-xl font-semibold text-secondary-900 dark:text-white">{stats?.securityPercentage || 0}%</p>
</div>
</div>
</div>
</div>
{/* Repositories List */}
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow">
<div className="px-6 py-4 border-b border-secondary-200 dark:border-secondary-700">
<h2 className="text-lg font-semibold text-secondary-900 dark:text-white">
Repositories ({filteredRepositories.length})
</h2>
</div>
{filteredRepositories.length === 0 ? (
<div className="px-6 py-12 text-center">
<Database className="mx-auto h-12 w-12 text-secondary-400" />
<h3 className="mt-2 text-sm font-medium text-secondary-900 dark:text-white">No repositories found</h3>
<p className="mt-1 text-sm text-secondary-500 dark:text-secondary-300">
{searchTerm || filterType !== 'all' || filterStatus !== 'all'
? 'Try adjusting your search or filters.'
: 'No repositories have been reported by your hosts yet.'}
</p>
<div className="card flex-1 flex flex-col overflow-hidden min-h-0">
<div className="px-4 py-4 sm:p-4 flex-1 flex flex-col overflow-hidden min-h-0">
<div className="flex items-center justify-end mb-4">
{/* Empty selection controls area to match packages page spacing */}
</div>
) : (
<div className="divide-y divide-secondary-200 dark:divide-secondary-700">
{filteredRepositories.map((repo) => (
<div key={repo.id} className="px-6 py-4 hover:bg-secondary-50 dark:hover:bg-secondary-700/50">
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
{repo.isSecure ? (
<Lock className="h-4 w-4 text-green-600" />
) : (
<Unlock className="h-4 w-4 text-orange-600" />
)}
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
{repo.name}
</h3>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
repo.isActive
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
}`}>
{repo.isActive ? 'Active' : 'Inactive'}
</span>
</div>
</div>
<div className="mt-2 space-y-1">
<p className="text-sm text-secondary-600 dark:text-secondary-300">
<Globe className="inline h-4 w-4 mr-1" />
{repo.url}
</p>
<div className="flex items-center gap-4 text-sm text-secondary-500 dark:text-secondary-400">
<span>Distribution: <span className="font-medium">{repo.distribution}</span></span>
<span>Type: <span className="font-medium">{repo.repoType}</span></span>
<span>Components: <span className="font-medium">{repo.components}</span></span>
</div>
</div>
</div>
<div className="flex items-center gap-4">
{/* Host Count */}
<div className="text-center">
<div className="flex items-center gap-1 text-sm text-secondary-500 dark:text-secondary-400">
<Users className="h-4 w-4" />
<span>{repo.hostCount} hosts</span>
</div>
</div>
{/* View Details */}
<Link
to={`/repositories/${repo.id}`}
className="btn-outline text-sm flex items-center gap-1"
>
<Eye className="h-4 w-4" />
View
</Link>
</div>
{/* Table Controls */}
<div className="mb-4 space-y-4">
<div className="flex flex-col sm:flex-row gap-4">
{/* Search */}
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-secondary-400 dark:text-secondary-500" />
<input
type="text"
placeholder="Search repositories..."
value={searchTerm}
onChange={(e) => 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"
/>
</div>
</div>
))}
{/* Security Filter */}
<div className="sm:w-48">
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
className="w-full px-3 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"
>
<option value="all">All Security Types</option>
<option value="secure">HTTPS Only</option>
<option value="insecure">HTTP Only</option>
</select>
</div>
{/* Status Filter */}
<div className="sm:w-48">
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="w-full px-3 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"
>
<option value="all">All Statuses</option>
<option value="active">Active Only</option>
<option value="inactive">Inactive Only</option>
</select>
</div>
{/* Columns Button */}
<div className="flex items-center">
<button
onClick={() => setShowColumnSettings(true)}
className="flex items-center gap-2 px-3 py-2 text-sm text-secondary-700 dark:text-secondary-300 bg-white dark:bg-secondary-700 border border-secondary-300 dark:border-secondary-600 rounded-md hover:bg-secondary-50 dark:hover:bg-secondary-600 transition-colors"
>
<Columns className="h-4 w-4" />
Columns
</button>
</div>
</div>
</div>
)}
<div className="flex-1 overflow-hidden">
{filteredAndSortedRepositories.length === 0 ? (
<div className="text-center py-8">
<Database className="h-12 w-12 text-secondary-400 mx-auto mb-4" />
<p className="text-secondary-500 dark:text-secondary-300">
{repositories?.length === 0 ? 'No repositories found' : 'No repositories match your filters'}
</p>
{repositories?.length === 0 && (
<p className="text-sm text-secondary-400 dark:text-secondary-400 mt-2">
No repositories have been reported by your hosts yet
</p>
)}
</div>
) : (
<div className="h-full overflow-auto">
<table className="min-w-full divide-y divide-secondary-200 dark:divide-secondary-600">
<thead className="bg-secondary-50 dark:bg-secondary-700 sticky top-0 z-10">
<tr>
{visibleColumns.map((column) => (
<th key={column.id} className="px-4 py-2 text-center text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
<button
onClick={() => handleSort(column.id)}
className="flex items-center gap-1 hover:text-secondary-700 dark:hover:text-secondary-200 transition-colors"
>
{column.label}
{getSortIcon(column.id)}
</button>
</th>
))}
</tr>
</thead>
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600">
{filteredAndSortedRepositories.map((repo) => (
<tr key={repo.id} className="hover:bg-secondary-50 dark:hover:bg-secondary-700 transition-colors">
{visibleColumns.map((column) => (
<td key={column.id} className="px-4 py-2 whitespace-nowrap text-center">
{renderCellContent(column, repo)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
{/* Column Settings Modal */}
{showColumnSettings && (
<ColumnSettingsModal
columnConfig={columnConfig}
onClose={() => setShowColumnSettings(false)}
onToggleVisibility={toggleColumnVisibility}
onReorder={reorderColumns}
onReset={resetColumns}
/>
)}
</div>
);
// Render cell content based on column type
function renderCellContent(column, repo) {
switch (column.id) {
case 'name':
return (
<div className="flex items-center">
<Database className="h-5 w-5 text-secondary-400 mr-3" />
<div>
<div className="text-sm font-medium text-secondary-900 dark:text-white">
{repo.name}
</div>
</div>
</div>
)
case 'url':
return (
<div className="text-sm text-secondary-900 dark:text-white max-w-xs truncate" title={repo.url}>
{repo.url}
</div>
)
case 'distribution':
return (
<div className="text-sm text-secondary-900 dark:text-white">
{repo.distribution}
</div>
)
case 'security':
const isSecure = repo.isSecure !== undefined ? repo.isSecure : repo.url.startsWith('https://');
return (
<div className="flex items-center justify-center">
{isSecure ? (
<div className="flex items-center gap-1 text-green-600">
<Lock className="h-4 w-4" />
<span className="text-sm">Secure</span>
</div>
) : (
<div className="flex items-center gap-1 text-orange-600">
<Unlock className="h-4 w-4" />
<span className="text-sm">Insecure</span>
</div>
)}
</div>
)
case 'status':
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
repo.isActive
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
}`}>
{repo.isActive ? 'Active' : 'Inactive'}
</span>
)
case 'hostCount':
return (
<div className="flex items-center justify-center gap-1 text-sm text-secondary-900 dark:text-white">
<Users className="h-4 w-4" />
<span>{repo.hostCount}</span>
</div>
)
case 'actions':
return (
<Link
to={`/repositories/${repo.id}`}
className="text-primary-600 hover:text-primary-900 flex items-center gap-1"
>
View
<Eye className="h-3 w-3" />
</Link>
)
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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-secondary-800 rounded-lg p-6 w-full max-w-md">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">Column Settings</h3>
<button onClick={onClose} className="text-secondary-400 hover:text-secondary-600 dark:text-secondary-500 dark:hover:text-secondary-300">
<X className="h-5 w-5" />
</button>
</div>
<div className="space-y-3">
{columnConfig.map((column, index) => (
<div
key={column.id}
draggable
onDragStart={(e) => 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"
>
<div className="flex items-center gap-3">
<GripVertical className="h-4 w-4 text-secondary-400" />
<span className="text-sm font-medium text-secondary-900 dark:text-white">
{column.label}
</span>
</div>
<button
onClick={() => onToggleVisibility(column.id)}
className={`w-4 h-4 rounded border-2 flex items-center justify-center ${
column.visible
? 'bg-primary-600 border-primary-600'
: 'bg-white dark:bg-secondary-800 border-secondary-300 dark:border-secondary-600'
}`}
>
{column.visible && <Check className="h-3 w-3 text-white" />}
</button>
</div>
))}
</div>
<div className="flex justify-between mt-6">
<button
onClick={onReset}
className="px-4 py-2 text-sm text-secondary-600 dark:text-secondary-400 hover:text-secondary-800 dark:hover:text-secondary-200"
>
Reset to Default
</button>
<button
onClick={onClose}
className="px-4 py-2 bg-primary-600 text-white text-sm rounded-md hover:bg-primary-700 transition-colors"
>
Done
</button>
</div>
</div>
</div>
)
};
export default Repositories;