mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-23 07:42:05 +00:00
Merge branch 'main' of github.com:9technologygroup/patchmon.net
This commit is contained in:
@@ -231,13 +231,14 @@ detect_os() {
|
||||
"opensuse"|"opensuse-leap"|"opensuse-tumbleweed")
|
||||
OS_TYPE="suse"
|
||||
;;
|
||||
"rocky"|"almalinux")
|
||||
"almalinux")
|
||||
OS_TYPE="rhel"
|
||||
;;
|
||||
"ol")
|
||||
# Keep Oracle Linux as 'ol' for proper frontend identification
|
||||
OS_TYPE="ol"
|
||||
;;
|
||||
# Rocky Linux keeps its own identity for proper frontend display
|
||||
esac
|
||||
|
||||
elif [[ -f /etc/redhat-release ]]; then
|
||||
@@ -265,7 +266,7 @@ get_repository_info() {
|
||||
"ubuntu"|"debian")
|
||||
get_apt_repositories repos_json first
|
||||
;;
|
||||
"centos"|"rhel"|"fedora"|"ol")
|
||||
"centos"|"rhel"|"fedora"|"ol"|"rocky")
|
||||
get_yum_repositories repos_json first
|
||||
;;
|
||||
*)
|
||||
@@ -573,14 +574,118 @@ get_yum_repositories() {
|
||||
local -n first_ref=$2
|
||||
|
||||
# Parse yum/dnf repository configuration
|
||||
local repo_info=""
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
local repo_info=$(dnf repolist all --verbose 2>/dev/null | grep -E "^Repo-id|^Repo-baseurl|^Repo-name|^Repo-status")
|
||||
repo_info=$(dnf repolist all --verbose 2>/dev/null | grep -E "^Repo-id|^Repo-baseurl|^Repo-mirrors|^Repo-name|^Repo-status")
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
local repo_info=$(yum repolist all -v 2>/dev/null | grep -E "^Repo-id|^Repo-baseurl|^Repo-name|^Repo-status")
|
||||
repo_info=$(yum repolist all -v 2>/dev/null | grep -E "^Repo-id|^Repo-baseurl|^Repo-mirrors|^Repo-name|^Repo-status")
|
||||
fi
|
||||
|
||||
# This is a simplified implementation - would need more work for full YUM support
|
||||
# For now, return empty for non-APT systems
|
||||
if [[ -z "$repo_info" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Parse repository information
|
||||
local current_repo=""
|
||||
local repo_id=""
|
||||
local repo_name=""
|
||||
local repo_url=""
|
||||
local repo_mirrors=""
|
||||
local repo_status=""
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^Repo-id[[:space:]]+:[[:space:]]+(.+)$ ]]; then
|
||||
# Process previous repository if we have one
|
||||
if [[ -n "$current_repo" ]]; then
|
||||
process_yum_repo repos_ref first_ref "$repo_id" "$repo_name" "$repo_url" "$repo_mirrors" "$repo_status"
|
||||
fi
|
||||
|
||||
# Start new repository
|
||||
repo_id="${BASH_REMATCH[1]}"
|
||||
repo_name="$repo_id"
|
||||
repo_url=""
|
||||
repo_mirrors=""
|
||||
repo_status=""
|
||||
current_repo="$repo_id"
|
||||
|
||||
elif [[ "$line" =~ ^Repo-name[[:space:]]+:[[:space:]]+(.+)$ ]]; then
|
||||
repo_name="${BASH_REMATCH[1]}"
|
||||
|
||||
elif [[ "$line" =~ ^Repo-baseurl[[:space:]]+:[[:space:]]+(.+)$ ]]; then
|
||||
repo_url="${BASH_REMATCH[1]}"
|
||||
|
||||
elif [[ "$line" =~ ^Repo-mirrors[[:space:]]+:[[:space:]]+(.+)$ ]]; then
|
||||
repo_mirrors="${BASH_REMATCH[1]}"
|
||||
|
||||
elif [[ "$line" =~ ^Repo-status[[:space:]]+:[[:space:]]+(.+)$ ]]; then
|
||||
repo_status="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
done <<< "$repo_info"
|
||||
|
||||
# Process the last repository
|
||||
if [[ -n "$current_repo" ]]; then
|
||||
process_yum_repo repos_ref first_ref "$repo_id" "$repo_name" "$repo_url" "$repo_mirrors" "$repo_status"
|
||||
fi
|
||||
}
|
||||
|
||||
# Process a single YUM repository and add it to the JSON
|
||||
process_yum_repo() {
|
||||
local -n _repos_ref=$1
|
||||
local -n _first_ref=$2
|
||||
local repo_id="$3"
|
||||
local repo_name="$4"
|
||||
local repo_url="$5"
|
||||
local repo_mirrors="$6"
|
||||
local repo_status="$7"
|
||||
|
||||
# Skip if we don't have essential info
|
||||
if [[ -z "$repo_id" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Determine if repository is enabled
|
||||
local is_enabled=false
|
||||
if [[ "$repo_status" == "enabled" ]]; then
|
||||
is_enabled=true
|
||||
fi
|
||||
|
||||
# Use baseurl if available, otherwise use mirrors URL
|
||||
local final_url=""
|
||||
if [[ -n "$repo_url" ]]; then
|
||||
# Extract first URL if multiple are listed
|
||||
final_url=$(echo "$repo_url" | head -n 1 | awk '{print $1}')
|
||||
elif [[ -n "$repo_mirrors" ]]; then
|
||||
final_url="$repo_mirrors"
|
||||
fi
|
||||
|
||||
# Skip if we don't have any URL
|
||||
if [[ -z "$final_url" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Determine if repository uses HTTPS
|
||||
local is_secure=false
|
||||
if [[ "$final_url" =~ ^https:// ]]; then
|
||||
is_secure=true
|
||||
fi
|
||||
|
||||
# Generate repository name if not provided
|
||||
if [[ -z "$repo_name" ]]; then
|
||||
repo_name="$repo_id"
|
||||
fi
|
||||
|
||||
# Clean up repository name and URL - escape quotes and backslashes
|
||||
repo_name=$(echo "$repo_name" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
|
||||
final_url=$(echo "$final_url" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
|
||||
|
||||
# Add to JSON
|
||||
if [[ "$_first_ref" == true ]]; then
|
||||
_first_ref=false
|
||||
else
|
||||
_repos_ref+=","
|
||||
fi
|
||||
|
||||
_repos_ref+="{\"name\":\"$repo_name\",\"url\":\"$final_url\",\"distribution\":\"$OS_VERSION\",\"components\":\"main\",\"repoType\":\"rpm\",\"isEnabled\":$is_enabled,\"isSecure\":$is_secure}"
|
||||
}
|
||||
|
||||
# Get package information based on OS
|
||||
@@ -592,7 +697,7 @@ get_package_info() {
|
||||
"ubuntu"|"debian")
|
||||
get_apt_packages packages_json first
|
||||
;;
|
||||
"centos"|"rhel"|"fedora"|"ol")
|
||||
"centos"|"rhel"|"fedora"|"ol"|"rocky")
|
||||
get_yum_packages packages_json first
|
||||
;;
|
||||
*)
|
||||
|
@@ -929,31 +929,36 @@ const Layout = ({ children }) => {
|
||||
<div className="h-6 w-px bg-secondary-200 dark:bg-secondary-600 lg:hidden" />
|
||||
|
||||
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
|
||||
{/* Page title - hidden on dashboard to give more space to search */}
|
||||
{location.pathname !== "/" && (
|
||||
<div className="relative flex items-center">
|
||||
<h2 className="text-lg font-semibold text-secondary-900 dark:text-secondary-100 whitespace-nowrap">
|
||||
{getPageTitle()}
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
{/* Page title - hidden on dashboard, hosts, repositories, packages, and host details to give more space to search */}
|
||||
{!["/", "/hosts", "/repositories", "/packages"].includes(
|
||||
location.pathname,
|
||||
) &&
|
||||
!location.pathname.startsWith("/hosts/") && (
|
||||
<div className="relative flex items-center">
|
||||
<h2 className="text-lg font-semibold text-secondary-900 dark:text-secondary-100 whitespace-nowrap">
|
||||
{getPageTitle()}
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Search Bar */}
|
||||
<div
|
||||
className={`flex items-center ${location.pathname === "/" ? "flex-1 max-w-none" : "max-w-sm"}`}
|
||||
className={`flex items-center ${["/", "/hosts", "/repositories", "/packages"].includes(location.pathname) || location.pathname.startsWith("/hosts/") ? "flex-1 max-w-none" : "max-w-sm"}`}
|
||||
>
|
||||
<GlobalSearch />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center gap-x-4 lg:gap-x-6 justify-end">
|
||||
{/* External Links */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<div className="hidden md:flex items-center gap-1">
|
||||
{/* 1) GitHub */}
|
||||
<a
|
||||
href="https://github.com/PatchMon/PatchMon"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-1.5 px-3 py-2 bg-gray-50 dark:bg-gray-800 text-secondary-600 dark:text-secondary-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors shadow-sm group relative"
|
||||
title="GitHub"
|
||||
aria-label="GitHub"
|
||||
>
|
||||
<Github className="h-5 w-5 flex-shrink-0" />
|
||||
{githubStars !== null && (
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
Clock,
|
||||
Copy,
|
||||
Cpu,
|
||||
Database,
|
||||
Eye,
|
||||
EyeOff,
|
||||
HardDrive,
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
dashboardAPI,
|
||||
formatDate,
|
||||
formatRelativeTime,
|
||||
repositoryAPI,
|
||||
settingsAPI,
|
||||
} from "../utils/api";
|
||||
import { OSIcon } from "../utils/osIcons.jsx";
|
||||
@@ -64,6 +66,15 @@ const HostDetail = () => {
|
||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||
});
|
||||
|
||||
// Fetch repository count for this host
|
||||
const { data: repositories, isLoading: isLoadingRepos } = useQuery({
|
||||
queryKey: ["host-repositories", hostId],
|
||||
queryFn: () => repositoryAPI.getByHost(hostId).then((res) => res.data),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes - data stays fresh longer
|
||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||
enabled: !!hostId,
|
||||
});
|
||||
|
||||
// Tab change handler
|
||||
const handleTabChange = (tabName) => {
|
||||
setActiveTab(tabName);
|
||||
@@ -290,7 +301,7 @@ const HostDetail = () => {
|
||||
</div>
|
||||
|
||||
{/* Package Statistics Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(`/packages?host=${hostId}`)}
|
||||
@@ -347,6 +358,25 @@ const HostDetail = () => {
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(`/repositories?host=${hostId}`)}
|
||||
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
|
||||
title="View repositories for this host"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Database className="h-5 w-5 text-blue-600 mr-2" />
|
||||
<div>
|
||||
<p className="text-sm text-secondary-500 dark:text-white">
|
||||
Repos
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{isLoadingRepos ? "..." : repositories?.length || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Main Content - Full Width */}
|
||||
|
@@ -18,21 +18,31 @@ import {
|
||||
Unlock,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { repositoryAPI } from "../utils/api";
|
||||
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 = [
|
||||
@@ -82,6 +92,17 @@ const Repositories = () => {
|
||||
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),
|
||||
@@ -202,7 +223,11 @@ const Repositories = () => {
|
||||
(filterStatus === "active" && repo.is_active === true) ||
|
||||
(filterStatus === "inactive" && repo.is_active === false);
|
||||
|
||||
return matchesSearch && matchesType && matchesStatus;
|
||||
// Filter by host if hostFilter is set
|
||||
const matchesHost =
|
||||
!hostFilter || repo.hosts?.some((host) => host.id === hostFilter);
|
||||
|
||||
return matchesSearch && matchesType && matchesStatus && matchesHost;
|
||||
});
|
||||
|
||||
// Sort repositories
|
||||
@@ -237,6 +262,7 @@ const Repositories = () => {
|
||||
filterStatus,
|
||||
sortField,
|
||||
sortDirection,
|
||||
hostFilter,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -421,6 +447,31 @@ const Repositories = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Host Filter Indicator */}
|
||||
{hostFilter && filteredHost && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-primary-50 dark:bg-primary-900 border border-primary-200 dark:border-primary-700 rounded-md">
|
||||
<Server className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span className="text-sm text-primary-700 dark:text-primary-300">
|
||||
Filtered by: {filteredHost.friendly_name}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setHostFilter("");
|
||||
// Update URL to remove host parameter
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.delete("host");
|
||||
navigate(`/repositories?${newSearchParams.toString()}`, {
|
||||
replace: true,
|
||||
});
|
||||
}}
|
||||
className="text-primary-500 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-200"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Security Filter */}
|
||||
<div className="sm:w-48">
|
||||
<select
|
||||
|
Reference in New Issue
Block a user