mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2026-01-05 04:29:36 -06:00
Merge pull request #124 from PatchMon/feat/repo_detail
restyle repository details
This commit is contained in:
@@ -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":
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user