Fix linting issues: remove unused imports, add button types, fix array keys

This commit is contained in:
Muhammad Ibrahim
2025-10-08 00:42:26 +01:00
parent ba9de097dc
commit a861e4f9eb
7 changed files with 953 additions and 7 deletions

View File

@@ -15,6 +15,7 @@ import Login from "./pages/Login";
import PackageDetail from "./pages/PackageDetail";
import Packages from "./pages/Packages";
import Profile from "./pages/Profile";
import Queue from "./pages/Queue";
import Repositories from "./pages/Repositories";
import RepositoryDetail from "./pages/RepositoryDetail";
import AlertChannels from "./pages/settings/AlertChannels";
@@ -115,6 +116,16 @@ function AppRoutes() {
</ProtectedRoute>
}
/>
<Route
path="/queue"
element={
<ProtectedRoute requirePermission="can_view_hosts">
<Layout>
<Queue />
</Layout>
</ProtectedRoute>
}
/>
<Route
path="/users"
element={

View File

@@ -11,6 +11,7 @@ import {
Github,
Globe,
Home,
List,
LogOut,
Mail,
Menu,
@@ -23,6 +24,7 @@ import {
Star,
UserCircle,
X,
Zap,
} from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { FaYoutube } from "react-icons/fa";
@@ -134,6 +136,22 @@ const Layout = ({ children }) => {
);
}
// Add Pro-Action and Queue items (available to all users with inventory access)
inventoryItems.push(
{
name: "Pro-Action",
href: "/pro-action",
icon: Zap,
comingSoon: true,
},
{
name: "Queue",
href: "/queue",
icon: List,
comingSoon: true,
},
);
if (inventoryItems.length > 0) {
nav.push({
section: "Inventory",
@@ -191,6 +209,8 @@ const Layout = ({ children }) => {
return "Repositories";
if (path === "/services") return "Services";
if (path === "/docker") return "Docker";
if (path === "/pro-action") return "Pro-Action";
if (path === "/queue") return "Queue";
if (path === "/users") return "Users";
if (path === "/permissions") return "Permissions";
if (path === "/settings") return "Settings";

View File

@@ -82,6 +82,7 @@ const SettingsLayout = ({ children }) => {
name: "Alert Channels",
href: "/settings/alert-channels",
icon: Bell,
comingSoon: true,
},
{
name: "Notifications",
@@ -118,7 +119,6 @@ const SettingsLayout = ({ children }) => {
name: "Integrations",
href: "/settings/integrations",
icon: Wrench,
comingSoon: true,
},
],
});

View File

@@ -6,6 +6,8 @@ import {
Chart as ChartJS,
Legend,
LinearScale,
LineElement,
PointElement,
Title,
Tooltip,
} from "chart.js";
@@ -23,7 +25,7 @@ import {
WifiOff,
} from "lucide-react";
import { useEffect, useState } from "react";
import { Bar, Doughnut, Pie } from "react-chartjs-2";
import { Bar, Doughnut, Line, Pie } from "react-chartjs-2";
import { useNavigate } from "react-router-dom";
import DashboardSettingsModal from "../components/DashboardSettingsModal";
import { useAuth } from "../contexts/AuthContext";
@@ -43,12 +45,16 @@ ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
);
const Dashboard = () => {
const [showSettingsModal, setShowSettingsModal] = useState(false);
const [cardPreferences, setCardPreferences] = useState([]);
const [packageTrendsPeriod, setPackageTrendsPeriod] = useState("1"); // days
const [packageTrendsHost, setPackageTrendsHost] = useState("all"); // host filter
const navigate = useNavigate();
const { isDark } = useTheme();
const { user } = useAuth();
@@ -91,7 +97,7 @@ const Dashboard = () => {
navigate("/repositories");
};
const handleOSDistributionClick = () => {
const _handleOSDistributionClick = () => {
navigate("/hosts?showFilters=true", { replace: true });
};
@@ -99,7 +105,7 @@ const Dashboard = () => {
navigate("/hosts?filter=needsUpdates", { replace: true });
};
const handlePackagePriorityClick = () => {
const _handlePackagePriorityClick = () => {
navigate("/packages?filter=security");
};
@@ -189,6 +195,26 @@ const Dashboard = () => {
refetchOnWindowFocus: false, // Don't refetch when window regains focus
});
// Package trends data query
const {
data: packageTrendsData,
isLoading: packageTrendsLoading,
error: _packageTrendsError,
} = useQuery({
queryKey: ["packageTrends", packageTrendsPeriod, packageTrendsHost],
queryFn: () => {
const params = {
days: packageTrendsPeriod,
};
if (packageTrendsHost !== "all") {
params.hostId = packageTrendsHost;
}
return dashboardAPI.getPackageTrends(params).then((res) => res.data);
},
staleTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: false,
});
// Fetch recent users (permission protected server-side)
const { data: recentUsers } = useQuery({
queryKey: ["dashboardRecentUsers"],
@@ -299,6 +325,8 @@ const Dashboard = () => {
].includes(cardId)
) {
return "charts";
} else if (["packageTrends"].includes(cardId)) {
return "charts";
} else if (["erroredHosts", "quickStats"].includes(cardId)) {
return "fullwidth";
}
@@ -312,6 +340,8 @@ const Dashboard = () => {
return "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4";
case "charts":
return "grid grid-cols-1 lg:grid-cols-3 gap-6";
case "widecharts":
return "grid grid-cols-1 lg:grid-cols-3 gap-6";
case "fullwidth":
return "space-y-6";
default:
@@ -733,6 +763,71 @@ const Dashboard = () => {
</div>
);
case "packageTrends":
return (
<div className="card p-6 w-full">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
Package Trends Over Time
</h3>
<div className="flex items-center gap-3">
{/* Period Selector */}
<select
value={packageTrendsPeriod}
onChange={(e) => setPackageTrendsPeriod(e.target.value)}
className="px-3 py-1.5 text-sm border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="1">Last 24 hours</option>
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
<option value="180">Last 6 months</option>
<option value="365">Last year</option>
</select>
{/* Host Selector */}
<select
value={packageTrendsHost}
onChange={(e) => setPackageTrendsHost(e.target.value)}
className="px-3 py-1.5 text-sm border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="all">All Hosts</option>
{packageTrendsData?.hosts?.length > 0 ? (
packageTrendsData.hosts.map((host) => (
<option key={host.id} value={host.id}>
{host.friendly_name || host.hostname}
</option>
))
) : (
<option disabled>
{packageTrendsLoading
? "Loading hosts..."
: "No hosts available"}
</option>
)}
</select>
</div>
</div>
<div className="h-64 w-full">
{packageTrendsLoading ? (
<div className="flex items-center justify-center h-full">
<RefreshCw className="h-8 w-8 animate-spin text-primary-600" />
</div>
) : packageTrendsData?.chartData ? (
<Line
data={packageTrendsData.chartData}
options={packageTrendsChartOptions}
/>
) : (
<div className="flex items-center justify-center h-full text-secondary-500 dark:text-secondary-400">
No data available
</div>
)}
</div>
</div>
);
case "quickStats": {
// Calculate dynamic stats
const updatePercentage =
@@ -1028,6 +1123,119 @@ const Dashboard = () => {
onClick: handlePackagePriorityChartClick,
};
const packageTrendsChartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "top",
labels: {
color: isDark ? "#ffffff" : "#374151",
font: {
size: 12,
},
padding: 20,
usePointStyle: true,
pointStyle: "circle",
},
},
tooltip: {
mode: "index",
intersect: false,
backgroundColor: isDark ? "#374151" : "#ffffff",
titleColor: isDark ? "#ffffff" : "#374151",
bodyColor: isDark ? "#ffffff" : "#374151",
borderColor: isDark ? "#4B5563" : "#E5E7EB",
borderWidth: 1,
callbacks: {
title: (context) => {
const label = context[0].label;
// Format hourly labels (e.g., "2025-10-07T14" -> "Oct 7, 2:00 PM")
if (label.includes("T")) {
const date = new Date(`${label}:00:00`);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
});
}
// Format daily labels (e.g., "2025-10-07" -> "Oct 7")
const date = new Date(label);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
},
},
},
},
scales: {
x: {
display: true,
title: {
display: true,
text: packageTrendsPeriod === "1" ? "Time (Hours)" : "Date",
color: isDark ? "#ffffff" : "#374151",
},
ticks: {
color: isDark ? "#ffffff" : "#374151",
font: {
size: 11,
},
callback: function (value, _index, _ticks) {
const label = this.getLabelForValue(value);
// Format hourly labels (e.g., "2025-10-07T14" -> "2 PM")
if (label.includes("T")) {
const hour = label.split("T")[1];
const hourNum = parseInt(hour, 10);
return hourNum === 0
? "12 AM"
: hourNum < 12
? `${hourNum} AM`
: hourNum === 12
? "12 PM"
: `${hourNum - 12} PM`;
}
// Format daily labels (e.g., "2025-10-07" -> "Oct 7")
const date = new Date(label);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
},
},
grid: {
color: isDark ? "#374151" : "#E5E7EB",
},
},
y: {
display: true,
title: {
display: true,
text: "Number of Packages",
color: isDark ? "#ffffff" : "#374151",
},
ticks: {
color: isDark ? "#ffffff" : "#374151",
font: {
size: 11,
},
beginAtZero: true,
},
grid: {
color: isDark ? "#374151" : "#E5E7EB",
},
},
},
interaction: {
mode: "nearest",
axis: "x",
intersect: false,
},
};
const barChartOptions = {
responsive: true,
indexAxis: "y", // Make the chart horizontal
@@ -1206,7 +1414,12 @@ const Dashboard = () => {
className={getGroupClassName(group.type)}
>
{group.cards.map((card, cardIndex) => (
<div key={`card-${card.cardId}-${groupIndex}-${cardIndex}`}>
<div
key={`card-${card.cardId}-${groupIndex}-${cardIndex}`}
className={
card.cardId === "packageTrends" ? "lg:col-span-2" : ""
}
>
{renderCard(card.cardId)}
</div>
))}

View File

@@ -5,8 +5,6 @@ import {
ArrowLeft,
Calendar,
CheckCircle,
ChevronDown,
ChevronUp,
Clock,
Copy,
Cpu,

View File

@@ -0,0 +1,699 @@
import {
Activity,
AlertCircle,
CheckCircle,
Clock,
Download,
Eye,
Filter,
Package,
Pause,
Play,
RefreshCw,
Search,
Server,
XCircle,
} from "lucide-react";
import { useState } from "react";
const Queue = () => {
const [activeTab, setActiveTab] = useState("server");
const [filterStatus, setFilterStatus] = useState("all");
const [searchQuery, setSearchQuery] = useState("");
// Mock data for demonstration
const serverQueueData = [
{
id: 1,
type: "Server Update Check",
description: "Check for server updates from GitHub",
status: "running",
priority: "high",
createdAt: "2024-01-15 10:30:00",
estimatedCompletion: "2024-01-15 10:35:00",
progress: 75,
retryCount: 0,
maxRetries: 3,
},
{
id: 2,
type: "Session Cleanup",
description: "Clear expired login sessions",
status: "pending",
priority: "medium",
createdAt: "2024-01-15 10:25:00",
estimatedCompletion: "2024-01-15 10:40:00",
progress: 0,
retryCount: 0,
maxRetries: 2,
},
{
id: 3,
type: "Database Optimization",
description: "Optimize database indexes and cleanup old records",
status: "completed",
priority: "low",
createdAt: "2024-01-15 09:00:00",
completedAt: "2024-01-15 09:45:00",
progress: 100,
retryCount: 0,
maxRetries: 1,
},
{
id: 4,
type: "Backup Creation",
description: "Create system backup",
status: "failed",
priority: "high",
createdAt: "2024-01-15 08:00:00",
errorMessage: "Insufficient disk space",
progress: 45,
retryCount: 2,
maxRetries: 3,
},
];
const agentQueueData = [
{
id: 1,
hostname: "web-server-01",
ip: "192.168.1.100",
type: "Agent Update Collection",
description: "Agent v1.2.7 → v1.2.8",
status: "pending",
priority: "medium",
lastCommunication: "2024-01-15 10:00:00",
nextExpectedCommunication: "2024-01-15 11:00:00",
currentVersion: "1.2.7",
targetVersion: "1.2.8",
retryCount: 0,
maxRetries: 5,
},
{
id: 2,
hostname: "db-server-02",
ip: "192.168.1.101",
type: "Data Collection",
description: "Collect package and system information",
status: "running",
priority: "high",
lastCommunication: "2024-01-15 10:15:00",
nextExpectedCommunication: "2024-01-15 11:15:00",
currentVersion: "1.2.8",
targetVersion: "1.2.8",
retryCount: 0,
maxRetries: 3,
},
{
id: 3,
hostname: "app-server-03",
ip: "192.168.1.102",
type: "Agent Update Collection",
description: "Agent v1.2.6 → v1.2.8",
status: "completed",
priority: "low",
lastCommunication: "2024-01-15 09:30:00",
completedAt: "2024-01-15 09:45:00",
currentVersion: "1.2.8",
targetVersion: "1.2.8",
retryCount: 0,
maxRetries: 5,
},
{
id: 4,
hostname: "test-server-04",
ip: "192.168.1.103",
type: "Data Collection",
description: "Collect package and system information",
status: "failed",
priority: "medium",
lastCommunication: "2024-01-15 08:00:00",
errorMessage: "Connection timeout",
retryCount: 3,
maxRetries: 3,
},
];
const patchQueueData = [
{
id: 1,
hostname: "web-server-01",
ip: "192.168.1.100",
packages: ["nginx", "openssl", "curl"],
type: "Security Updates",
description: "Apply critical security patches",
status: "pending",
priority: "high",
scheduledFor: "2024-01-15 19:00:00",
lastCommunication: "2024-01-15 18:00:00",
nextExpectedCommunication: "2024-01-15 19:00:00",
retryCount: 0,
maxRetries: 3,
},
{
id: 2,
hostname: "db-server-02",
ip: "192.168.1.101",
packages: ["postgresql", "python3"],
type: "Feature Updates",
description: "Update database and Python packages",
status: "running",
priority: "medium",
scheduledFor: "2024-01-15 20:00:00",
lastCommunication: "2024-01-15 19:15:00",
nextExpectedCommunication: "2024-01-15 20:15:00",
retryCount: 0,
maxRetries: 2,
},
{
id: 3,
hostname: "app-server-03",
ip: "192.168.1.102",
packages: ["nodejs", "npm"],
type: "Maintenance Updates",
description: "Update Node.js and npm packages",
status: "completed",
priority: "low",
scheduledFor: "2024-01-15 18:30:00",
completedAt: "2024-01-15 18:45:00",
retryCount: 0,
maxRetries: 2,
},
{
id: 4,
hostname: "test-server-04",
ip: "192.168.1.103",
packages: ["docker", "docker-compose"],
type: "Security Updates",
description: "Update Docker components",
status: "failed",
priority: "high",
scheduledFor: "2024-01-15 17:00:00",
errorMessage: "Package conflicts detected",
retryCount: 2,
maxRetries: 3,
},
];
const getStatusIcon = (status) => {
switch (status) {
case "running":
return <RefreshCw className="h-4 w-4 text-blue-500 animate-spin" />;
case "completed":
return <CheckCircle className="h-4 w-4 text-green-500" />;
case "failed":
return <XCircle className="h-4 w-4 text-red-500" />;
case "pending":
return <Clock className="h-4 w-4 text-yellow-500" />;
case "paused":
return <Pause className="h-4 w-4 text-gray-500" />;
default:
return <AlertCircle className="h-4 w-4 text-gray-500" />;
}
};
const getStatusColor = (status) => {
switch (status) {
case "running":
return "bg-blue-100 text-blue-800";
case "completed":
return "bg-green-100 text-green-800";
case "failed":
return "bg-red-100 text-red-800";
case "pending":
return "bg-yellow-100 text-yellow-800";
case "paused":
return "bg-gray-100 text-gray-800";
default:
return "bg-gray-100 text-gray-800";
}
};
const getPriorityColor = (priority) => {
switch (priority) {
case "high":
return "bg-red-100 text-red-800";
case "medium":
return "bg-yellow-100 text-yellow-800";
case "low":
return "bg-green-100 text-green-800";
default:
return "bg-gray-100 text-gray-800";
}
};
const filteredData = (data) => {
let filtered = data;
if (filterStatus !== "all") {
filtered = filtered.filter((item) => item.status === filterStatus);
}
if (searchQuery) {
filtered = filtered.filter(
(item) =>
item.hostname?.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.type?.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description?.toLowerCase().includes(searchQuery.toLowerCase()),
);
}
return filtered;
};
const tabs = [
{
id: "server",
name: "Server Queue",
icon: Server,
data: serverQueueData,
count: serverQueueData.length,
},
{
id: "agent",
name: "Agent Queue",
icon: Download,
data: agentQueueData,
count: agentQueueData.length,
},
{
id: "patch",
name: "Patch Management",
icon: Package,
data: patchQueueData,
count: patchQueueData.length,
},
];
const renderServerQueueItem = (item) => (
<div
key={item.id}
className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
{getStatusIcon(item.status)}
<h3 className="font-medium text-gray-900 dark:text-white">
{item.type}
</h3>
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getStatusColor(item.status)}`}
>
{item.status}
</span>
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getPriorityColor(item.priority)}`}
>
{item.priority}
</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{item.description}
</p>
{item.status === "running" && (
<div className="mb-3">
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span>Progress</span>
<span>{item.progress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${item.progress}%` }}
></div>
</div>
</div>
)}
<div className="grid grid-cols-2 gap-4 text-xs text-gray-500">
<div>
<span className="font-medium">Created:</span> {item.createdAt}
</div>
{item.status === "running" && (
<div>
<span className="font-medium">ETA:</span>{" "}
{item.estimatedCompletion}
</div>
)}
{item.status === "completed" && (
<div>
<span className="font-medium">Completed:</span>{" "}
{item.completedAt}
</div>
)}
{item.status === "failed" && (
<div className="col-span-2">
<span className="font-medium">Error:</span> {item.errorMessage}
</div>
)}
</div>
{item.retryCount > 0 && (
<div className="mt-2 text-xs text-orange-600">
Retries: {item.retryCount}/{item.maxRetries}
</div>
)}
</div>
<div className="flex gap-2 ml-4">
{item.status === "running" && (
<button
type="button"
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<Pause className="h-4 w-4" />
</button>
)}
{item.status === "paused" && (
<button
type="button"
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<Play className="h-4 w-4" />
</button>
)}
{item.status === "failed" && (
<button
type="button"
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<RefreshCw className="h-4 w-4" />
</button>
)}
<button
type="button"
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<Eye className="h-4 w-4" />
</button>
</div>
</div>
</div>
);
const renderAgentQueueItem = (item) => (
<div
key={item.id}
className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
{getStatusIcon(item.status)}
<h3 className="font-medium text-gray-900 dark:text-white">
{item.hostname}
</h3>
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getStatusColor(item.status)}`}
>
{item.status}
</span>
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getPriorityColor(item.priority)}`}
>
{item.priority}
</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
{item.type}
</p>
<p className="text-sm text-gray-500 mb-3">{item.description}</p>
{item.type === "Agent Update Collection" && (
<div className="mb-3 p-2 bg-gray-50 dark:bg-gray-700 rounded">
<div className="text-xs text-gray-600 dark:text-gray-400">
<span className="font-medium">Version:</span>{" "}
{item.currentVersion} {item.targetVersion}
</div>
</div>
)}
<div className="grid grid-cols-2 gap-4 text-xs text-gray-500">
<div>
<span className="font-medium">IP:</span> {item.ip}
</div>
<div>
<span className="font-medium">Last Comm:</span>{" "}
{item.lastCommunication}
</div>
<div>
<span className="font-medium">Next Expected:</span>{" "}
{item.nextExpectedCommunication}
</div>
{item.status === "completed" && (
<div>
<span className="font-medium">Completed:</span>{" "}
{item.completedAt}
</div>
)}
{item.status === "failed" && (
<div className="col-span-2">
<span className="font-medium">Error:</span> {item.errorMessage}
</div>
)}
</div>
{item.retryCount > 0 && (
<div className="mt-2 text-xs text-orange-600">
Retries: {item.retryCount}/{item.maxRetries}
</div>
)}
</div>
<div className="flex gap-2 ml-4">
{item.status === "failed" && (
<button
type="button"
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<RefreshCw className="h-4 w-4" />
</button>
)}
<button
type="button"
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<Eye className="h-4 w-4" />
</button>
</div>
</div>
</div>
);
const renderPatchQueueItem = (item) => (
<div
key={item.id}
className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
{getStatusIcon(item.status)}
<h3 className="font-medium text-gray-900 dark:text-white">
{item.hostname}
</h3>
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getStatusColor(item.status)}`}
>
{item.status}
</span>
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getPriorityColor(item.priority)}`}
>
{item.priority}
</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
{item.type}
</p>
<p className="text-sm text-gray-500 mb-3">{item.description}</p>
<div className="mb-3">
<div className="text-xs text-gray-600 dark:text-gray-400 mb-1">
<span className="font-medium">Packages:</span>
</div>
<div className="flex flex-wrap gap-1">
{item.packages.map((pkg) => (
<span
key={pkg}
className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded"
>
{pkg}
</span>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-4 text-xs text-gray-500">
<div>
<span className="font-medium">IP:</span> {item.ip}
</div>
<div>
<span className="font-medium">Scheduled:</span>{" "}
{item.scheduledFor}
</div>
<div>
<span className="font-medium">Last Comm:</span>{" "}
{item.lastCommunication}
</div>
<div>
<span className="font-medium">Next Expected:</span>{" "}
{item.nextExpectedCommunication}
</div>
{item.status === "completed" && (
<div>
<span className="font-medium">Completed:</span>{" "}
{item.completedAt}
</div>
)}
{item.status === "failed" && (
<div className="col-span-2">
<span className="font-medium">Error:</span> {item.errorMessage}
</div>
)}
</div>
{item.retryCount > 0 && (
<div className="mt-2 text-xs text-orange-600">
Retries: {item.retryCount}/{item.maxRetries}
</div>
)}
</div>
<div className="flex gap-2 ml-4">
{item.status === "failed" && (
<button
type="button"
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<RefreshCw className="h-4 w-4" />
</button>
)}
<button
type="button"
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<Eye className="h-4 w-4" />
</button>
</div>
</div>
</div>
);
const currentTab = tabs.find((tab) => tab.id === activeTab);
const filteredItems = filteredData(currentTab?.data || []);
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Queue Management
</h1>
<p className="text-gray-600 dark:text-gray-400">
Monitor and manage server operations, agent communications, and
patch deployments
</p>
</div>
{/* Tabs */}
<div className="mb-6">
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<button
type="button"
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 ${
activeTab === tab.id
? "border-blue-500 text-blue-600 dark:text-blue-400"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
}`}
>
<tab.icon className="h-4 w-4" />
{tab.name}
<span className="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 px-2 py-0.5 rounded-full text-xs">
{tab.count}
</span>
</button>
))}
</nav>
</div>
</div>
{/* Filters and Search */}
<div className="mb-6 flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<input
type="text"
placeholder="Search queues..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">All Status</option>
<option value="pending">Pending</option>
<option value="running">Running</option>
<option value="completed">Completed</option>
<option value="failed">Failed</option>
<option value="paused">Paused</option>
</select>
<button
type="button"
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-2"
>
<Filter className="h-4 w-4" />
More Filters
</button>
</div>
</div>
{/* Queue Items */}
<div className="space-y-4">
{filteredItems.length === 0 ? (
<div className="text-center py-12">
<Activity className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">
No queue items found
</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{searchQuery
? "Try adjusting your search criteria"
: "No items match the current filters"}
</p>
</div>
) : (
filteredItems.map((item) => {
switch (activeTab) {
case "server":
return renderServerQueueItem(item);
case "agent":
return renderAgentQueueItem(item);
case "patch":
return renderPatchQueueItem(item);
default:
return null;
}
})
)}
</div>
</div>
</div>
);
};
export default Queue;

View File

@@ -56,6 +56,11 @@ export const dashboardAPI = {
const url = `/dashboard/hosts/${hostId}${queryString ? `?${queryString}` : ""}`;
return api.get(url);
},
getPackageTrends: (params = {}) => {
const queryString = new URLSearchParams(params).toString();
const url = `/dashboard/package-trends${queryString ? `?${queryString}` : ""}`;
return api.get(url);
},
getRecentUsers: () => api.get("/dashboard/recent-users"),
getRecentCollection: () => api.get("/dashboard/recent-collection"),
};