Merge pull request #124 from PatchMon/feat/repo_detail

restyle repository details
This commit is contained in:
9 Technology Group LTD
2025-10-03 18:03:23 +01:00
committed by GitHub
2 changed files with 203 additions and 81 deletions

View File

@@ -16,7 +16,6 @@ import {
Shield,
ShieldCheck,
Unlock,
Users,
X,
} from "lucide-react";
import { useMemo, useState } from "react";
@@ -513,8 +512,8 @@ const Repositories = () => {
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.host_count}</span>
<Server className="h-4 w-4" />
<span>{repo.hostCount}</span>
</div>
);
case "actions":

View File

@@ -6,17 +6,17 @@ import {
Database,
Globe,
Lock,
Search,
Server,
Shield,
ShieldOff,
Unlock,
Users,
} from "lucide-react";
import { useId, useState } from "react";
import { useId, useMemo, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { repositoryAPI } from "../utils/api";
import { Link, useNavigate, useParams } from "react-router-dom";
import { formatRelativeTime, repositoryAPI } from "../utils/api";
const RepositoryDetail = () => {
const isActiveId = useId();
@@ -25,8 +25,12 @@ const RepositoryDetail = () => {
const descriptionId = useId();
const { repositoryId } = useParams();
const queryClient = useQueryClient();
const navigate = useNavigate();
const [editMode, setEditMode] = useState(false);
const [formData, setFormData] = useState({});
const [searchTerm, setSearchTerm] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(25);
// Fetch repository details
const {
@@ -39,6 +43,49 @@ const RepositoryDetail = () => {
enabled: !!repositoryId,
});
const hosts = repository?.host_repositories || [];
// Filter and paginate hosts
const filteredAndPaginatedHosts = useMemo(() => {
let filtered = hosts;
if (searchTerm) {
filtered = hosts.filter(
(hostRepo) =>
hostRepo.hosts.friendly_name
?.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
hostRepo.hosts.hostname
?.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
hostRepo.hosts.ip?.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return filtered.slice(startIndex, endIndex);
}, [hosts, searchTerm, currentPage, pageSize]);
const totalPages = Math.ceil(
(searchTerm
? hosts.filter(
(hostRepo) =>
hostRepo.hosts.friendly_name
?.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
hostRepo.hosts.hostname
?.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
hostRepo.hosts.ip?.toLowerCase().includes(searchTerm.toLowerCase()),
).length
: hosts.length) / pageSize,
);
const handleHostClick = (hostId) => {
navigate(`/hosts/${hostId}`);
};
// Update repository mutation
const updateRepositoryMutation = useMutation({
mutationFn: (data) => repositoryAPI.update(repositoryId, data),
@@ -157,9 +204,6 @@ const RepositoryDetail = () => {
{repository.is_active ? "Active" : "Inactive"}
</span>
</div>
<p className="text-secondary-500 dark:text-secondary-300 mt-1">
Repository configuration and host assignments
</p>
</div>
</div>
<div className="flex items-center gap-2">
@@ -193,7 +237,7 @@ const RepositoryDetail = () => {
</div>
{/* Repository Information */}
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow">
<div className="card">
<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">
Repository Information
@@ -369,80 +413,159 @@ const RepositoryDetail = () => {
</div>
{/* Hosts Using This Repository */}
<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 flex items-center gap-2">
<Users className="h-5 w-5" />
Hosts Using This Repository (
{repository.host_repositories?.length || 0})
</h2>
</div>
{!repository.host_repositories ||
repository.host_repositories.length === 0 ? (
<div className="px-6 py-12 text-center">
<Server 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 hosts using this repository
</h3>
<p className="mt-1 text-sm text-secondary-500 dark:text-secondary-300">
This repository hasn't been reported by any hosts yet.
</p>
<div className="card">
<div className="px-6 py-4 border-b border-secondary-200 dark:border-secondary-600">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<Server className="h-5 w-5 text-primary-600" />
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
Hosts Using This Repository ({hosts.length})
</h3>
</div>
</div>
) : (
<div className="divide-y divide-secondary-200 dark:divide-secondary-700">
{repository.host_repositories.map((hostRepo) => (
<div
key={hostRepo.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 items-center gap-3">
<div
className={`w-3 h-3 rounded-full ${
hostRepo.hosts.status === "active"
? "bg-green-500"
: hostRepo.hosts.status === "pending"
? "bg-yellow-500"
: "bg-red-500"
}`}
/>
<div>
<Link
to={`/hosts/${hostRepo.hosts.id}`}
className="text-primary-600 hover:text-primary-700 font-medium"
>
{hostRepo.hosts.friendly_name}
</Link>
<div className="flex items-center gap-4 text-sm text-secondary-500 dark:text-secondary-400 mt-1">
<span>IP: {hostRepo.hosts.ip}</span>
<span>
OS: {hostRepo.hosts.os_type}{" "}
{hostRepo.hosts.os_version}
</span>
<span>
Last Update:{" "}
{new Date(
hostRepo.hosts.last_update,
).toLocaleDateString()}
</span>
</div>
</div>
{/* Search */}
<div className="relative max-w-sm">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-secondary-400" />
<input
type="text"
placeholder="Search hosts..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
}}
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>
<div className="overflow-x-auto">
{filteredAndPaginatedHosts.length === 0 ? (
<div className="text-center py-8">
<Server className="h-12 w-12 text-secondary-400 mx-auto mb-4" />
<p className="text-secondary-500 dark:text-secondary-300">
{searchTerm
? "No hosts match your search"
: "This repository hasn't been reported by any hosts yet."}
</p>
</div>
) : (
<>
<table className="min-w-full divide-y divide-secondary-200 dark:divide-secondary-600">
<thead className="bg-secondary-50 dark:bg-secondary-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
Host
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
Operating System
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
Last Checked
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
Last Update
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600">
{filteredAndPaginatedHosts.map((hostRepo) => (
<tr
key={hostRepo.id}
className="hover:bg-secondary-50 dark:hover:bg-secondary-700 cursor-pointer transition-colors"
onClick={() => handleHostClick(hostRepo.hosts.id)}
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div
className={`w-2 h-2 rounded-full mr-3 ${
hostRepo.hosts.status === "active"
? "bg-success-500"
: hostRepo.hosts.status === "pending"
? "bg-warning-500"
: "bg-danger-500"
}`}
/>
<Server className="h-5 w-5 text-secondary-400 mr-3" />
<div>
<div className="text-sm font-medium text-secondary-900 dark:text-white">
{hostRepo.hosts.friendly_name ||
hostRepo.hosts.hostname}
</div>
{hostRepo.hosts.friendly_name &&
hostRepo.hosts.hostname && (
<div className="text-sm text-secondary-500 dark:text-secondary-300">
{hostRepo.hosts.hostname}
</div>
)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
{hostRepo.hosts.os_type} {hostRepo.hosts.os_version}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500 dark:text-secondary-300">
{hostRepo.last_checked
? formatRelativeTime(hostRepo.last_checked)
: "Never"}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500 dark:text-secondary-300">
{hostRepo.hosts.last_update
? formatRelativeTime(hostRepo.hosts.last_update)
: "Never"}
</td>
</tr>
))}
</tbody>
</table>
{/* Pagination */}
{totalPages > 1 && (
<div className="px-6 py-3 bg-white dark:bg-secondary-800 border-t border-secondary-200 dark:border-secondary-600 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-secondary-700 dark:text-secondary-300">
Rows per page:
</span>
<select
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));
setCurrentPage(1);
}}
className="text-sm border border-secondary-300 dark:border-secondary-600 rounded px-2 py-1 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
</div>
<div className="flex items-center gap-4">
<div className="text-center">
<div className="text-xs text-secondary-500 dark:text-secondary-400">
Last Checked
</div>
<div className="text-sm text-secondary-900 dark:text-white">
{new Date(hostRepo.last_checked).toLocaleDateString()}
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setCurrentPage(currentPage - 1)}
disabled={currentPage === 1}
className="px-3 py-1 text-sm border border-secondary-300 dark:border-secondary-600 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-secondary-50 dark:hover:bg-secondary-700"
>
Previous
</button>
<span className="text-sm text-secondary-700 dark:text-secondary-300">
Page {currentPage} of {totalPages}
</span>
<button
type="button"
onClick={() => setCurrentPage(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-3 py-1 text-sm border border-secondary-300 dark:border-secondary-600 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-secondary-50 dark:hover:bg-secondary-700"
>
Next
</button>
</div>
</div>
</div>
))}
</div>
)}
)}
</>
)}
</div>
</div>
</div>
);