mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-02 21:13:45 +00:00
Building the start of Automation page and implemented BullMQ module
This commit is contained in:
@@ -18,7 +18,7 @@ const Login = lazy(() => import("./pages/Login"));
|
||||
const PackageDetail = lazy(() => import("./pages/PackageDetail"));
|
||||
const Packages = lazy(() => import("./pages/Packages"));
|
||||
const Profile = lazy(() => import("./pages/Profile"));
|
||||
const Queue = lazy(() => import("./pages/Queue"));
|
||||
const Automation = lazy(() => import("./pages/Automation"));
|
||||
const Repositories = lazy(() => import("./pages/Repositories"));
|
||||
const RepositoryDetail = lazy(() => import("./pages/RepositoryDetail"));
|
||||
const AlertChannels = lazy(() => import("./pages/settings/AlertChannels"));
|
||||
@@ -137,11 +137,11 @@ function AppRoutes() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/queue"
|
||||
path="/automation"
|
||||
element={
|
||||
<ProtectedRoute requirePermission="can_view_hosts">
|
||||
<Layout>
|
||||
<Queue />
|
||||
<Automation />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ const Layout = ({ children }) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Add Pro-Action and Queue items (available to all users with inventory access)
|
||||
// Add Pro-Action and Automation items (available to all users with inventory access)
|
||||
inventoryItems.push(
|
||||
{
|
||||
name: "Pro-Action",
|
||||
@@ -145,8 +145,8 @@ const Layout = ({ children }) => {
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
name: "Queue",
|
||||
href: "/queue",
|
||||
name: "Automation",
|
||||
href: "/automation",
|
||||
icon: List,
|
||||
comingSoon: true,
|
||||
},
|
||||
@@ -210,7 +210,7 @@ const Layout = ({ children }) => {
|
||||
if (path === "/services") return "Services";
|
||||
if (path === "/docker") return "Docker";
|
||||
if (path === "/pro-action") return "Pro-Action";
|
||||
if (path === "/queue") return "Queue";
|
||||
if (path === "/automation") return "Automation";
|
||||
if (path === "/users") return "Users";
|
||||
if (path === "/permissions") return "Permissions";
|
||||
if (path === "/settings") return "Settings";
|
||||
@@ -929,10 +929,14 @@ 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, hosts, repositories, packages, and host details to give more space to search */}
|
||||
{!["/", "/hosts", "/repositories", "/packages"].includes(
|
||||
location.pathname,
|
||||
) &&
|
||||
{/* Page title - hidden on dashboard, hosts, repositories, packages, automation, and host details to give more space to search */}
|
||||
{![
|
||||
"/",
|
||||
"/hosts",
|
||||
"/repositories",
|
||||
"/packages",
|
||||
"/automation",
|
||||
].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">
|
||||
@@ -943,7 +947,7 @@ const Layout = ({ children }) => {
|
||||
|
||||
{/* Global Search Bar */}
|
||||
<div
|
||||
className={`flex items-center ${["/", "/hosts", "/repositories", "/packages"].includes(location.pathname) || location.pathname.startsWith("/hosts/") ? "flex-1 max-w-none" : "max-w-sm"}`}
|
||||
className={`flex items-center ${["/", "/hosts", "/repositories", "/packages", "/automation"].includes(location.pathname) || location.pathname.startsWith("/hosts/") ? "flex-1 max-w-none" : "max-w-sm"}`}
|
||||
>
|
||||
<GlobalSearch />
|
||||
</div>
|
||||
|
||||
581
frontend/src/pages/Automation.jsx
Normal file
581
frontend/src/pages/Automation.jsx
Normal file
@@ -0,0 +1,581 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Activity,
|
||||
AlertCircle,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
ArrowUpDown,
|
||||
Bot,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Play,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
XCircle,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import api from "../utils/api";
|
||||
|
||||
const Automation = () => {
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [sortField, setSortField] = useState("nextRunTimestamp");
|
||||
const [sortDirection, setSortDirection] = useState("asc");
|
||||
|
||||
// Fetch automation overview data
|
||||
const { data: overview, isLoading: overviewLoading } = useQuery({
|
||||
queryKey: ["automation-overview"],
|
||||
queryFn: async () => {
|
||||
const response = await api.get("/automation/overview");
|
||||
return response.data.data;
|
||||
},
|
||||
refetchInterval: 30000, // Refresh every 30 seconds
|
||||
});
|
||||
|
||||
// Fetch queue statistics
|
||||
const { data: queueStats, isLoading: statsLoading } = useQuery({
|
||||
queryKey: ["automation-stats"],
|
||||
queryFn: async () => {
|
||||
const response = await api.get("/automation/stats");
|
||||
return response.data.data;
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
// Fetch recent jobs
|
||||
const { data: recentJobs, isLoading: jobsLoading } = useQuery({
|
||||
queryKey: ["automation-jobs"],
|
||||
queryFn: async () => {
|
||||
const jobs = await Promise.all([
|
||||
api
|
||||
.get("/automation/jobs/github-update-check?limit=5")
|
||||
.then((r) => r.data.data || []),
|
||||
api
|
||||
.get("/automation/jobs/session-cleanup?limit=5")
|
||||
.then((r) => r.data.data || []),
|
||||
]);
|
||||
return {
|
||||
githubUpdate: jobs[0],
|
||||
sessionCleanup: jobs[1],
|
||||
};
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
switch (status) {
|
||||
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 "active":
|
||||
return <Activity className="h-4 w-4 text-blue-500 animate-pulse" />;
|
||||
default:
|
||||
return <Clock className="h-4 w-4 text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return "bg-green-100 text-green-800";
|
||||
case "failed":
|
||||
return "bg-red-100 text-red-800";
|
||||
case "active":
|
||||
return "bg-blue-100 text-blue-800";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "N/A";
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
const formatDuration = (ms) => {
|
||||
if (!ms) return "N/A";
|
||||
return `${ms}ms`;
|
||||
};
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
switch (status) {
|
||||
case "Success":
|
||||
return (
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800">
|
||||
Success
|
||||
</span>
|
||||
);
|
||||
case "Failed":
|
||||
return (
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800">
|
||||
Failed
|
||||
</span>
|
||||
);
|
||||
case "Never run":
|
||||
return (
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-800">
|
||||
Never run
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-800">
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getNextRunTime = (schedule, lastRun) => {
|
||||
if (schedule === "Manual only") return "Manual trigger only";
|
||||
if (schedule === "Daily at midnight") {
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(0, 0, 0, 0);
|
||||
return tomorrow.toLocaleString([], {
|
||||
hour12: true,
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
if (schedule === "Daily at 2 AM") {
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(2, 0, 0, 0);
|
||||
return tomorrow.toLocaleString([], {
|
||||
hour12: true,
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
if (schedule === "Every hour") {
|
||||
const now = new Date();
|
||||
const nextHour = new Date(now);
|
||||
nextHour.setHours(nextHour.getHours() + 1, 0, 0, 0);
|
||||
return nextHour.toLocaleString([], {
|
||||
hour12: true,
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
return "Unknown";
|
||||
};
|
||||
|
||||
const getNextRunTimestamp = (schedule) => {
|
||||
if (schedule === "Manual only") return Number.MAX_SAFE_INTEGER; // Manual tasks go to bottom
|
||||
if (schedule === "Daily at midnight") {
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(0, 0, 0, 0);
|
||||
return tomorrow.getTime();
|
||||
}
|
||||
if (schedule === "Daily at 2 AM") {
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(2, 0, 0, 0);
|
||||
return tomorrow.getTime();
|
||||
}
|
||||
if (schedule === "Every hour") {
|
||||
const now = new Date();
|
||||
const nextHour = new Date(now);
|
||||
nextHour.setHours(nextHour.getHours() + 1, 0, 0, 0);
|
||||
return nextHour.getTime();
|
||||
}
|
||||
return Number.MAX_SAFE_INTEGER; // Unknown schedules go to bottom
|
||||
};
|
||||
|
||||
const triggerManualJob = async (jobType, data = {}) => {
|
||||
try {
|
||||
let endpoint;
|
||||
|
||||
if (jobType === "github") {
|
||||
endpoint = "/automation/trigger/github-update";
|
||||
} else if (jobType === "sessions") {
|
||||
endpoint = "/automation/trigger/session-cleanup";
|
||||
} else if (jobType === "echo") {
|
||||
endpoint = "/automation/trigger/echo-hello";
|
||||
} else if (jobType === "orphaned-repos") {
|
||||
endpoint = "/automation/trigger/orphaned-repo-cleanup";
|
||||
}
|
||||
|
||||
const response = await api.post(endpoint, data);
|
||||
|
||||
// Refresh data
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error("Error triggering job:", error);
|
||||
alert(
|
||||
"Failed to trigger job: " +
|
||||
(error.response?.data?.error || error.message),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSort = (field) => {
|
||||
if (sortField === field) {
|
||||
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection("asc");
|
||||
}
|
||||
};
|
||||
|
||||
const getSortIcon = (field) => {
|
||||
if (sortField !== field) return <ArrowUpDown className="h-4 w-4" />;
|
||||
return sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
);
|
||||
};
|
||||
|
||||
// Sort automations based on current sort settings
|
||||
const sortedAutomations = overview?.automations
|
||||
? [...overview.automations].sort((a, b) => {
|
||||
let aValue, bValue;
|
||||
|
||||
switch (sortField) {
|
||||
case "name":
|
||||
aValue = a.name.toLowerCase();
|
||||
bValue = b.name.toLowerCase();
|
||||
break;
|
||||
case "schedule":
|
||||
aValue = a.schedule.toLowerCase();
|
||||
bValue = b.schedule.toLowerCase();
|
||||
break;
|
||||
case "lastRun":
|
||||
// Convert "Never" to empty string for proper sorting
|
||||
aValue = a.lastRun === "Never" ? "" : a.lastRun;
|
||||
bValue = b.lastRun === "Never" ? "" : b.lastRun;
|
||||
break;
|
||||
case "lastRunTimestamp":
|
||||
aValue = a.lastRunTimestamp || 0;
|
||||
bValue = b.lastRunTimestamp || 0;
|
||||
break;
|
||||
case "nextRunTimestamp":
|
||||
aValue = getNextRunTimestamp(a.schedule);
|
||||
bValue = getNextRunTimestamp(b.schedule);
|
||||
break;
|
||||
case "status":
|
||||
aValue = a.status.toLowerCase();
|
||||
bValue = b.status.toLowerCase();
|
||||
break;
|
||||
default:
|
||||
aValue = a[sortField];
|
||||
bValue = b[sortField];
|
||||
}
|
||||
|
||||
if (aValue < bValue) return sortDirection === "asc" ? -1 : 1;
|
||||
if (aValue > bValue) return sortDirection === "asc" ? 1 : -1;
|
||||
return 0;
|
||||
})
|
||||
: [];
|
||||
|
||||
const tabs = [{ id: "overview", name: "Overview", icon: Settings }];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-secondary-900 dark:text-white">
|
||||
Automation Management
|
||||
</h1>
|
||||
<p className="text-sm text-secondary-600 dark:text-secondary-400 mt-1">
|
||||
Monitor and manage automated server operations, agent
|
||||
communications, and patch deployments
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => triggerManualJob("github")}
|
||||
className="btn-outline flex items-center gap-2"
|
||||
title="Trigger manual GitHub update check"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Check Updates
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => triggerManualJob("sessions")}
|
||||
className="btn-outline flex items-center gap-2"
|
||||
title="Trigger manual session cleanup"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Clean Sessions
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
triggerManualJob("echo", {
|
||||
message: "Hello from Automation Page!",
|
||||
})
|
||||
}
|
||||
className="btn-outline flex items-center gap-2"
|
||||
title="Trigger echo hello task"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Echo Hello
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{/* Scheduled Tasks Card */}
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<Clock className="h-5 w-5 text-warning-600 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">
|
||||
Scheduled Tasks
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{overviewLoading ? "..." : overview?.scheduledTasks || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Running Tasks Card */}
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<Play className="h-5 w-5 text-success-600 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">
|
||||
Running Tasks
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{overviewLoading ? "..." : overview?.runningTasks || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Failed Tasks Card */}
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<XCircle className="h-5 w-5 text-red-600 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">
|
||||
Failed Tasks
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{overviewLoading ? "..." : overview?.failedTasks || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total Task Runs Card */}
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<Zap className="h-5 w-5 text-secondary-600 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">
|
||||
Total Task Runs
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{overviewLoading ? "..." : overview?.totalAutomations || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === "overview" && (
|
||||
<div className="card p-6">
|
||||
{overviewLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-2 text-sm text-secondary-500">
|
||||
Loading automations...
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-secondary-200 dark:divide-secondary-600">
|
||||
<thead className="bg-secondary-50 dark:bg-secondary-700">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
|
||||
Run
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider cursor-pointer hover:bg-secondary-100 dark:hover:bg-secondary-600"
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Task
|
||||
{getSortIcon("name")}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider cursor-pointer hover:bg-secondary-100 dark:hover:bg-secondary-600"
|
||||
onClick={() => handleSort("schedule")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Frequency
|
||||
{getSortIcon("schedule")}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider cursor-pointer hover:bg-secondary-100 dark:hover:bg-secondary-600"
|
||||
onClick={() => handleSort("lastRunTimestamp")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Last Run
|
||||
{getSortIcon("lastRunTimestamp")}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider cursor-pointer hover:bg-secondary-100 dark:hover:bg-secondary-600"
|
||||
onClick={() => handleSort("nextRunTimestamp")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Next Run
|
||||
{getSortIcon("nextRunTimestamp")}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider cursor-pointer hover:bg-secondary-100 dark:hover:bg-secondary-600"
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Status
|
||||
{getSortIcon("status")}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600">
|
||||
{sortedAutomations.map((automation) => (
|
||||
<tr
|
||||
key={automation.queue}
|
||||
className="hover:bg-secondary-50 dark:hover:bg-secondary-700"
|
||||
>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
{automation.schedule !== "Manual only" ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (automation.queue.includes("github")) {
|
||||
triggerManualJob("github");
|
||||
} else if (automation.queue.includes("session")) {
|
||||
triggerManualJob("sessions");
|
||||
} else if (automation.queue.includes("echo")) {
|
||||
triggerManualJob("echo", {
|
||||
message: "Manual trigger from table",
|
||||
});
|
||||
} else if (
|
||||
automation.queue.includes("orphaned-repo")
|
||||
) {
|
||||
triggerManualJob("orphaned-repos");
|
||||
}
|
||||
}}
|
||||
className="inline-flex items-center justify-center w-6 h-6 border border-transparent rounded text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-colors duration-200"
|
||||
title="Run Now"
|
||||
>
|
||||
<Play className="h-3 w-3" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (automation.queue.includes("echo")) {
|
||||
triggerManualJob("echo", {
|
||||
message: "Manual trigger from table",
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="inline-flex items-center justify-center w-6 h-6 border border-transparent rounded text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-colors duration-200"
|
||||
title="Trigger"
|
||||
>
|
||||
<Play className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-secondary-900 dark:text-white">
|
||||
{automation.name}
|
||||
</div>
|
||||
<div className="text-xs text-secondary-500 dark:text-secondary-400">
|
||||
{automation.description}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
|
||||
{automation.schedule}
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
|
||||
{automation.lastRun}
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-secondary-900 dark:text-white">
|
||||
{getNextRunTime(
|
||||
automation.schedule,
|
||||
automation.lastRun,
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
{getStatusBadge(automation.status)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Automation;
|
||||
@@ -1,699 +0,0 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user