From b454b8d13039243acf3a4b4fdd6a301ab0559f0e Mon Sep 17 00:00:00 2001
From: tigattack <10629864+tigattack@users.noreply.github.com>
Date: Wed, 1 Oct 2025 02:01:38 +0100
Subject: [PATCH] feat(packages): show all packages by default, add pagination
---
backend/src/routes/packageRoutes.js | 18 +-
frontend/src/pages/Packages.jsx | 296 ++++++++++++++++++++++------
2 files changed, 244 insertions(+), 70 deletions(-)
diff --git a/backend/src/routes/packageRoutes.js b/backend/src/routes/packageRoutes.js
index e611bd3..e948526 100644
--- a/backend/src/routes/packageRoutes.js
+++ b/backend/src/routes/packageRoutes.js
@@ -67,7 +67,9 @@ router.get("/", async (req, res) => {
latest_version: true,
created_at: true,
_count: {
- host_packages: true,
+ select: {
+ host_packages: true,
+ },
},
},
skip,
@@ -82,7 +84,7 @@ router.get("/", async (req, res) => {
// Get additional stats for each package
const packagesWithStats = await Promise.all(
packages.map(async (pkg) => {
- const [updatesCount, securityCount, affectedHosts] = await Promise.all([
+ const [updatesCount, securityCount, packageHosts] = await Promise.all([
prisma.host_packages.count({
where: {
package_id: pkg.id,
@@ -117,17 +119,17 @@ router.get("/", async (req, res) => {
return {
...pkg,
- affectedHostsCount: pkg._count.hostPackages,
- affectedHosts: affectedHosts.map((hp) => ({
- hostId: hp.host.id,
- friendlyName: hp.host.friendly_name,
- osType: hp.host.os_type,
+ packageHostsCount: pkg._count.host_packages,
+ packageHosts: packageHosts.map((hp) => ({
+ hostId: hp.hosts.id,
+ friendlyName: hp.hosts.friendly_name,
+ osType: hp.hosts.os_type,
currentVersion: hp.current_version,
availableVersion: hp.available_version,
isSecurityUpdate: hp.is_security_update,
})),
stats: {
- totalInstalls: pkg._count.hostPackages,
+ totalInstalls: pkg._count.host_packages,
updatesNeeded: updatesCount,
securityUpdates: securityCount,
},
diff --git a/frontend/src/pages/Packages.jsx b/frontend/src/pages/Packages.jsx
index ebb088d..75ca544 100644
--- a/frontend/src/pages/Packages.jsx
+++ b/frontend/src/pages/Packages.jsx
@@ -4,6 +4,8 @@ import {
ArrowDown,
ArrowUp,
ArrowUpDown,
+ ChevronLeft,
+ ChevronRight,
Columns,
Eye as EyeIcon,
EyeOff as EyeOffIcon,
@@ -17,16 +19,28 @@ import {
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
-import { dashboardAPI } from "../utils/api";
+import { dashboardAPI, packagesAPI } from "../utils/api";
const Packages = () => {
const [searchTerm, setSearchTerm] = useState("");
const [categoryFilter, setCategoryFilter] = useState("all");
- const [securityFilter, setSecurityFilter] = useState("all");
+ const [updateStatusFilter, setUpdateStatusFilter] = useState("all-packages");
const [hostFilter, setHostFilter] = useState("all");
const [sortField, setSortField] = useState("name");
const [sortDirection, setSortDirection] = useState("asc");
const [showColumnSettings, setShowColumnSettings] = useState(false);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(() => {
+ const saved = localStorage.getItem("packages-page-size");
+ if (saved) {
+ const parsedSize = parseInt(saved, 10);
+ // Validate that the saved page size is one of the allowed values
+ if ([25, 50, 100, 200].includes(parsedSize)) {
+ return parsedSize;
+ }
+ }
+ return 25; // Default fallback
+ });
const [searchParams] = useSearchParams();
const navigate = useNavigate();
@@ -42,8 +56,8 @@ const Packages = () => {
const [columnConfig, setColumnConfig] = useState(() => {
const defaultConfig = [
{ id: "name", label: "Package", visible: true, order: 0 },
- { id: "affectedHosts", label: "Affected Hosts", visible: true, order: 1 },
- { id: "priority", label: "Priority", visible: true, order: 2 },
+ { id: "packageHosts", label: "Installed On", visible: true, order: 1 },
+ { id: "status", label: "Status", visible: true, order: 2 },
{ id: "latestVersion", label: "Latest Version", visible: true, order: 3 },
];
@@ -65,10 +79,10 @@ const Packages = () => {
localStorage.setItem("packages-column-config", JSON.stringify(newConfig));
};
- // Handle affected hosts click
- const handleAffectedHostsClick = (pkg) => {
- const affectedHosts = pkg.affectedHosts || [];
- const hostIds = affectedHosts.map((host) => host.hostId);
+ // Handle hosts click (view hosts where package is installed)
+ const handlePackageHostsClick = (pkg) => {
+ const packageHosts = pkg.packageHosts || [];
+ const hostIds = packageHosts.map((host) => host.hostId);
// Create URL with selected hosts and filter
const params = new URLSearchParams();
@@ -86,27 +100,43 @@ const Packages = () => {
// For outdated packages, we want to show all packages that need updates
// This is the default behavior, so we don't need to change filters
setCategoryFilter("all");
- setSecurityFilter("all");
+ setUpdateStatusFilter("needs-updates");
} else if (filter === "security") {
// For security updates, filter to show only security updates
- setSecurityFilter("security");
+ setUpdateStatusFilter("security-updates");
setCategoryFilter("all");
}
}, [searchParams]);
const {
- data: packages,
+ data: packagesResponse,
isLoading,
error,
refetch,
isFetching,
} = useQuery({
queryKey: ["packages"],
- queryFn: () => dashboardAPI.getPackages().then((res) => res.data),
+ queryFn: () => packagesAPI.getAll({ limit: 1000 }).then((res) => res.data),
staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
refetchOnWindowFocus: false, // Don't refetch when window regains focus
});
+ // Extract packages from the response and normalise the data structure
+ const packages = useMemo(() => {
+ if (!packagesResponse?.packages) return [];
+
+ return packagesResponse.packages.map((pkg) => ({
+ ...pkg,
+ // Normalise field names to match the frontend expectations
+ packageHostsCount: pkg.packageHostsCount || pkg.stats?.totalInstalls || 0,
+ latestVersion: pkg.latest_version || pkg.latestVersion || "Unknown",
+ isUpdatable: (pkg.stats?.updatesNeeded || 0) > 0,
+ isSecurityUpdate: (pkg.stats?.securityUpdates || 0) > 0,
+ // Ensure we have hosts array (for packages, this contains all hosts where the package is installed)
+ packageHosts: pkg.packageHosts || [],
+ }));
+ }, [packagesResponse]);
+
// Fetch hosts data to get total packages count
const { data: hosts } = useQuery({
queryKey: ["hosts"],
@@ -128,17 +158,30 @@ const Packages = () => {
const matchesCategory =
categoryFilter === "all" || pkg.category === categoryFilter;
- const matchesSecurity =
- securityFilter === "all" ||
- (securityFilter === "security" && pkg.isSecurityUpdate) ||
- (securityFilter === "regular" && !pkg.isSecurityUpdate);
+ const matchesUpdateStatus =
+ updateStatusFilter === "all-packages" ||
+ updateStatusFilter === "needs-updates" ||
+ (updateStatusFilter === "security-updates" && pkg.isSecurityUpdate) ||
+ (updateStatusFilter === "regular-updates" && !pkg.isSecurityUpdate);
- const affectedHosts = pkg.affectedHosts || [];
+ // For "all-packages", we don't filter by update status
+ // For other filters, we only show packages that need updates
+ const matchesUpdateNeeded =
+ updateStatusFilter === "all-packages" ||
+ (pkg.stats?.updatesNeeded || 0) > 0;
+
+ const packageHosts = pkg.packageHosts || [];
const matchesHost =
hostFilter === "all" ||
- affectedHosts.some((host) => host.hostId === hostFilter);
+ packageHosts.some((host) => host.hostId === hostFilter);
- return matchesSearch && matchesCategory && matchesSecurity && matchesHost;
+ return (
+ matchesSearch &&
+ matchesCategory &&
+ matchesUpdateStatus &&
+ matchesUpdateNeeded &&
+ matchesHost
+ );
});
// Sorting
@@ -154,14 +197,38 @@ const Packages = () => {
aValue = a.latestVersion?.toLowerCase() || "";
bValue = b.latestVersion?.toLowerCase() || "";
break;
- case "affectedHosts":
- aValue = a.affectedHostsCount || a.affectedHosts?.length || 0;
- bValue = b.affectedHostsCount || b.affectedHosts?.length || 0;
+ case "packageHosts":
+ aValue = a.packageHostsCount || a.packageHosts?.length || 0;
+ bValue = b.packageHostsCount || b.packageHosts?.length || 0;
break;
- case "priority":
- aValue = a.isSecurityUpdate ? 0 : 1; // Security updates first
- bValue = b.isSecurityUpdate ? 0 : 1;
+ case "status": {
+ // Handle sorting for the three status states: Up to Date, Update Available, Security Update Available
+ const aNeedsUpdates = (a.stats?.updatesNeeded || 0) > 0;
+ const bNeedsUpdates = (b.stats?.updatesNeeded || 0) > 0;
+
+ // Define priority order: Security Update (0) > Regular Update (1) > Up to Date (2)
+ let aPriority, bPriority;
+
+ if (!aNeedsUpdates) {
+ aPriority = 2; // Up to Date
+ } else if (a.isSecurityUpdate) {
+ aPriority = 0; // Security Update
+ } else {
+ aPriority = 1; // Regular Update
+ }
+
+ if (!bNeedsUpdates) {
+ bPriority = 2; // Up to Date
+ } else if (b.isSecurityUpdate) {
+ bPriority = 0; // Security Update
+ } else {
+ bPriority = 1; // Regular Update
+ }
+
+ aValue = aPriority;
+ bValue = bPriority;
break;
+ }
default:
aValue = a.name?.toLowerCase() || "";
bValue = b.name?.toLowerCase() || "";
@@ -177,12 +244,33 @@ const Packages = () => {
packages,
searchTerm,
categoryFilter,
- securityFilter,
+ updateStatusFilter,
sortField,
sortDirection,
hostFilter,
]);
+ // Calculate pagination
+ const totalPages = Math.ceil(filteredAndSortedPackages.length / pageSize);
+ const startIndex = (currentPage - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const paginatedPackages = filteredAndSortedPackages.slice(
+ startIndex,
+ endIndex,
+ );
+
+ // Reset to first page when filters or page size change
+ // biome-ignore lint/correctness/useExhaustiveDependencies: We want this effect to run when filter values or page size change to reset pagination
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [searchTerm, categoryFilter, updateStatusFilter, hostFilter, pageSize]);
+
+ // Function to handle page size change and save to localStorage
+ const handlePageSizeChange = (newPageSize) => {
+ setPageSize(newPageSize);
+ localStorage.setItem("packages-page-size", newPageSize.toString());
+ };
+
// Get visible columns in order
const visibleColumns = columnConfig
.filter((col) => col.visible)
@@ -231,8 +319,8 @@ const Packages = () => {
const resetColumns = () => {
const defaultConfig = [
{ id: "name", label: "Package", visible: true, order: 0 },
- { id: "affectedHosts", label: "Affected Hosts", visible: true, order: 1 },
- { id: "priority", label: "Priority", visible: true, order: 2 },
+ { id: "packageHosts", label: "Installed On", visible: true, order: 1 },
+ { id: "status", label: "Status", visible: true, order: 2 },
{ id: "latestVersion", label: "Latest Version", visible: true, order: 3 },
];
updateColumnConfig(defaultConfig);
@@ -262,31 +350,56 @@ const Packages = () => {
);
- case "affectedHosts": {
- const affectedHostsCount =
- pkg.affectedHostsCount || pkg.affectedHosts?.length || 0;
+ case "packageHosts": {
+ // Show total number of hosts where this package is installed
+ const installedHostsCount =
+ pkg.packageHostsCount ||
+ pkg.stats?.totalInstalls ||
+ pkg.packageHosts?.length ||
+ 0;
+ // For packages that need updates, show how many need updates
+ const hostsNeedingUpdates = pkg.stats?.updatesNeeded || 0;
+
+ const displayText =
+ hostsNeedingUpdates > 0 && hostsNeedingUpdates < installedHostsCount
+ ? `${hostsNeedingUpdates}/${installedHostsCount} hosts`
+ : `${installedHostsCount} host${installedHostsCount !== 1 ? "s" : ""}`;
+
+ const titleText =
+ hostsNeedingUpdates > 0 && hostsNeedingUpdates < installedHostsCount
+ ? `${hostsNeedingUpdates} of ${installedHostsCount} hosts need updates`
+ : `Installed on ${installedHostsCount} host${installedHostsCount !== 1 ? "s" : ""}`;
+
return (
);
}
- case "priority":
+ case "status": {
+ // Check if this package needs updates
+ const needsUpdates = (pkg.stats?.updatesNeeded || 0) > 0;
+
+ if (!needsUpdates) {
+ return Up to Date;
+ }
+
return pkg.isSecurityUpdate ? (
- {uniqueAffectedHostsCount} + {uniquePackageHostsCount}
{packages?.length === 0 - ? "No packages need updates" + ? "No packages found" : "No packages match your filters"}
{packages?.length === 0 && (- All packages are up to date across all hosts + Packages will appear here once hosts start reporting their + installed packages
)} @@ -571,7 +692,7 @@ const Packages = () => { - {filteredAndSortedPackages.map((pkg) => ( + {paginatedPackages.map((pkg) => (