mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-05 06:23:22 +00:00
Fix linting issues: remove unused imports, add button types, fix array keys
This commit is contained in:
@@ -15,6 +15,7 @@ import Login from "./pages/Login";
|
|||||||
import PackageDetail from "./pages/PackageDetail";
|
import PackageDetail from "./pages/PackageDetail";
|
||||||
import Packages from "./pages/Packages";
|
import Packages from "./pages/Packages";
|
||||||
import Profile from "./pages/Profile";
|
import Profile from "./pages/Profile";
|
||||||
|
import Queue from "./pages/Queue";
|
||||||
import Repositories from "./pages/Repositories";
|
import Repositories from "./pages/Repositories";
|
||||||
import RepositoryDetail from "./pages/RepositoryDetail";
|
import RepositoryDetail from "./pages/RepositoryDetail";
|
||||||
import AlertChannels from "./pages/settings/AlertChannels";
|
import AlertChannels from "./pages/settings/AlertChannels";
|
||||||
@@ -115,6 +116,16 @@ function AppRoutes() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/queue"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute requirePermission="can_view_hosts">
|
||||||
|
<Layout>
|
||||||
|
<Queue />
|
||||||
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/users"
|
path="/users"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Github,
|
Github,
|
||||||
Globe,
|
Globe,
|
||||||
Home,
|
Home,
|
||||||
|
List,
|
||||||
LogOut,
|
LogOut,
|
||||||
Mail,
|
Mail,
|
||||||
Menu,
|
Menu,
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
Star,
|
Star,
|
||||||
UserCircle,
|
UserCircle,
|
||||||
X,
|
X,
|
||||||
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { FaYoutube } from "react-icons/fa";
|
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) {
|
if (inventoryItems.length > 0) {
|
||||||
nav.push({
|
nav.push({
|
||||||
section: "Inventory",
|
section: "Inventory",
|
||||||
@@ -191,6 +209,8 @@ const Layout = ({ children }) => {
|
|||||||
return "Repositories";
|
return "Repositories";
|
||||||
if (path === "/services") return "Services";
|
if (path === "/services") return "Services";
|
||||||
if (path === "/docker") return "Docker";
|
if (path === "/docker") return "Docker";
|
||||||
|
if (path === "/pro-action") return "Pro-Action";
|
||||||
|
if (path === "/queue") return "Queue";
|
||||||
if (path === "/users") return "Users";
|
if (path === "/users") return "Users";
|
||||||
if (path === "/permissions") return "Permissions";
|
if (path === "/permissions") return "Permissions";
|
||||||
if (path === "/settings") return "Settings";
|
if (path === "/settings") return "Settings";
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const SettingsLayout = ({ children }) => {
|
|||||||
name: "Alert Channels",
|
name: "Alert Channels",
|
||||||
href: "/settings/alert-channels",
|
href: "/settings/alert-channels",
|
||||||
icon: Bell,
|
icon: Bell,
|
||||||
|
comingSoon: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Notifications",
|
name: "Notifications",
|
||||||
@@ -118,7 +119,6 @@ const SettingsLayout = ({ children }) => {
|
|||||||
name: "Integrations",
|
name: "Integrations",
|
||||||
href: "/settings/integrations",
|
href: "/settings/integrations",
|
||||||
icon: Wrench,
|
icon: Wrench,
|
||||||
comingSoon: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
Legend,
|
Legend,
|
||||||
LinearScale,
|
LinearScale,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "chart.js";
|
} from "chart.js";
|
||||||
@@ -23,7 +25,7 @@ import {
|
|||||||
WifiOff,
|
WifiOff,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect, useState } from "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 { useNavigate } from "react-router-dom";
|
||||||
import DashboardSettingsModal from "../components/DashboardSettingsModal";
|
import DashboardSettingsModal from "../components/DashboardSettingsModal";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
@@ -43,12 +45,16 @@ ChartJS.register(
|
|||||||
CategoryScale,
|
CategoryScale,
|
||||||
LinearScale,
|
LinearScale,
|
||||||
BarElement,
|
BarElement,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
Title,
|
Title,
|
||||||
);
|
);
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
||||||
const [cardPreferences, setCardPreferences] = useState([]);
|
const [cardPreferences, setCardPreferences] = useState([]);
|
||||||
|
const [packageTrendsPeriod, setPackageTrendsPeriod] = useState("1"); // days
|
||||||
|
const [packageTrendsHost, setPackageTrendsHost] = useState("all"); // host filter
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -91,7 +97,7 @@ const Dashboard = () => {
|
|||||||
navigate("/repositories");
|
navigate("/repositories");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOSDistributionClick = () => {
|
const _handleOSDistributionClick = () => {
|
||||||
navigate("/hosts?showFilters=true", { replace: true });
|
navigate("/hosts?showFilters=true", { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,7 +105,7 @@ const Dashboard = () => {
|
|||||||
navigate("/hosts?filter=needsUpdates", { replace: true });
|
navigate("/hosts?filter=needsUpdates", { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePackagePriorityClick = () => {
|
const _handlePackagePriorityClick = () => {
|
||||||
navigate("/packages?filter=security");
|
navigate("/packages?filter=security");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -189,6 +195,26 @@ const Dashboard = () => {
|
|||||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
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)
|
// Fetch recent users (permission protected server-side)
|
||||||
const { data: recentUsers } = useQuery({
|
const { data: recentUsers } = useQuery({
|
||||||
queryKey: ["dashboardRecentUsers"],
|
queryKey: ["dashboardRecentUsers"],
|
||||||
@@ -299,6 +325,8 @@ const Dashboard = () => {
|
|||||||
].includes(cardId)
|
].includes(cardId)
|
||||||
) {
|
) {
|
||||||
return "charts";
|
return "charts";
|
||||||
|
} else if (["packageTrends"].includes(cardId)) {
|
||||||
|
return "charts";
|
||||||
} else if (["erroredHosts", "quickStats"].includes(cardId)) {
|
} else if (["erroredHosts", "quickStats"].includes(cardId)) {
|
||||||
return "fullwidth";
|
return "fullwidth";
|
||||||
}
|
}
|
||||||
@@ -312,6 +340,8 @@ const Dashboard = () => {
|
|||||||
return "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4";
|
return "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4";
|
||||||
case "charts":
|
case "charts":
|
||||||
return "grid grid-cols-1 lg:grid-cols-3 gap-6";
|
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":
|
case "fullwidth":
|
||||||
return "space-y-6";
|
return "space-y-6";
|
||||||
default:
|
default:
|
||||||
@@ -733,6 +763,71 @@ const Dashboard = () => {
|
|||||||
</div>
|
</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": {
|
case "quickStats": {
|
||||||
// Calculate dynamic stats
|
// Calculate dynamic stats
|
||||||
const updatePercentage =
|
const updatePercentage =
|
||||||
@@ -1028,6 +1123,119 @@ const Dashboard = () => {
|
|||||||
onClick: handlePackagePriorityChartClick,
|
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 = {
|
const barChartOptions = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
indexAxis: "y", // Make the chart horizontal
|
indexAxis: "y", // Make the chart horizontal
|
||||||
@@ -1206,7 +1414,12 @@ const Dashboard = () => {
|
|||||||
className={getGroupClassName(group.type)}
|
className={getGroupClassName(group.type)}
|
||||||
>
|
>
|
||||||
{group.cards.map((card, cardIndex) => (
|
{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)}
|
{renderCard(card.cardId)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import {
|
|||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Calendar,
|
Calendar,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
ChevronDown,
|
|
||||||
ChevronUp,
|
|
||||||
Clock,
|
Clock,
|
||||||
Copy,
|
Copy,
|
||||||
Cpu,
|
Cpu,
|
||||||
|
|||||||
699
frontend/src/pages/Queue.jsx
Normal file
699
frontend/src/pages/Queue.jsx
Normal 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;
|
||||||
@@ -56,6 +56,11 @@ export const dashboardAPI = {
|
|||||||
const url = `/dashboard/hosts/${hostId}${queryString ? `?${queryString}` : ""}`;
|
const url = `/dashboard/hosts/${hostId}${queryString ? `?${queryString}` : ""}`;
|
||||||
return api.get(url);
|
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"),
|
getRecentUsers: () => api.get("/dashboard/recent-users"),
|
||||||
getRecentCollection: () => api.get("/dashboard/recent-collection"),
|
getRecentCollection: () => api.get("/dashboard/recent-collection"),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user