mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-23 07:42:05 +00:00
Improved patchmon-agent.sh logic to handle locked apt processes
Introduced docker Feature integration via agent
This commit is contained in:
@@ -21,6 +21,12 @@ const Profile = lazy(() => import("./pages/Profile"));
|
||||
const Automation = lazy(() => import("./pages/Automation"));
|
||||
const Repositories = lazy(() => import("./pages/Repositories"));
|
||||
const RepositoryDetail = lazy(() => import("./pages/RepositoryDetail"));
|
||||
const Docker = lazy(() => import("./pages/Docker"));
|
||||
const DockerContainerDetail = lazy(
|
||||
() => import("./pages/docker/ContainerDetail"),
|
||||
);
|
||||
const DockerImageDetail = lazy(() => import("./pages/docker/ImageDetail"));
|
||||
const DockerHostDetail = lazy(() => import("./pages/docker/HostDetail"));
|
||||
const AlertChannels = lazy(() => import("./pages/settings/AlertChannels"));
|
||||
const Integrations = lazy(() => import("./pages/settings/Integrations"));
|
||||
const Notifications = lazy(() => import("./pages/settings/Notifications"));
|
||||
@@ -146,6 +152,46 @@ function AppRoutes() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/docker"
|
||||
element={
|
||||
<ProtectedRoute requirePermission="can_view_reports">
|
||||
<Layout>
|
||||
<Docker />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/docker/containers/:id"
|
||||
element={
|
||||
<ProtectedRoute requirePermission="can_view_reports">
|
||||
<Layout>
|
||||
<DockerContainerDetail />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/docker/images/:id"
|
||||
element={
|
||||
<ProtectedRoute requirePermission="can_view_reports">
|
||||
<Layout>
|
||||
<DockerImageDetail />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/docker/hosts/:id"
|
||||
element={
|
||||
<ProtectedRoute requirePermission="can_view_reports">
|
||||
<Layout>
|
||||
<DockerHostDetail />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/users"
|
||||
element={
|
||||
|
@@ -11,7 +11,6 @@ import {
|
||||
Github,
|
||||
Globe,
|
||||
Home,
|
||||
List,
|
||||
LogOut,
|
||||
Mail,
|
||||
Menu,
|
||||
@@ -113,18 +112,26 @@ const Layout = ({ children }) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Add Automation item (available to all users with inventory access)
|
||||
inventoryItems.push({
|
||||
name: "Automation",
|
||||
href: "/automation",
|
||||
icon: RefreshCw,
|
||||
beta: true,
|
||||
});
|
||||
|
||||
if (canViewReports()) {
|
||||
inventoryItems.push(
|
||||
{
|
||||
name: "Services",
|
||||
href: "/services",
|
||||
icon: Activity,
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
name: "Docker",
|
||||
href: "/docker",
|
||||
icon: Container,
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
name: "Services",
|
||||
href: "/services",
|
||||
icon: Activity,
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
@@ -136,20 +143,13 @@ const Layout = ({ children }) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Add Pro-Action and Automation items (available to all users with inventory access)
|
||||
inventoryItems.push(
|
||||
{
|
||||
name: "Pro-Action",
|
||||
href: "/pro-action",
|
||||
icon: Zap,
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
name: "Automation",
|
||||
href: "/automation",
|
||||
icon: List,
|
||||
},
|
||||
);
|
||||
// Add Pro-Action item (available to all users with inventory access)
|
||||
inventoryItems.push({
|
||||
name: "Pro-Action",
|
||||
href: "/pro-action",
|
||||
icon: Zap,
|
||||
comingSoon: true,
|
||||
});
|
||||
|
||||
if (inventoryItems.length > 0) {
|
||||
nav.push({
|
||||
@@ -435,6 +435,11 @@ const Layout = ({ children }) => {
|
||||
Soon
|
||||
</span>
|
||||
)}
|
||||
{subItem.beta && (
|
||||
<span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-200 px-1.5 py-0.5 rounded font-medium">
|
||||
Beta
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
@@ -706,6 +711,11 @@ const Layout = ({ children }) => {
|
||||
Soon
|
||||
</span>
|
||||
)}
|
||||
{subItem.beta && (
|
||||
<span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-200 px-1.5 py-0.5 rounded font-medium">
|
||||
Beta
|
||||
</span>
|
||||
)}
|
||||
{subItem.showUpgradeIcon && (
|
||||
<UpgradeNotificationIcon className="h-3 w-3" />
|
||||
)}
|
||||
@@ -928,15 +938,17 @@ 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, automation, and host details to give more space to search */}
|
||||
{/* Page title - hidden on dashboard, hosts, repositories, packages, automation, docker, and host details to give more space to search */}
|
||||
{![
|
||||
"/",
|
||||
"/hosts",
|
||||
"/repositories",
|
||||
"/packages",
|
||||
"/automation",
|
||||
"/docker",
|
||||
].includes(location.pathname) &&
|
||||
!location.pathname.startsWith("/hosts/") && (
|
||||
!location.pathname.startsWith("/hosts/") &&
|
||||
!location.pathname.startsWith("/docker/") && (
|
||||
<div className="relative flex items-center">
|
||||
<h2 className="text-lg font-semibold text-secondary-900 dark:text-secondary-100 whitespace-nowrap">
|
||||
{getPageTitle()}
|
||||
@@ -946,7 +958,7 @@ const Layout = ({ children }) => {
|
||||
|
||||
{/* Global Search Bar */}
|
||||
<div
|
||||
className={`flex items-center ${["/", "/hosts", "/repositories", "/packages", "/automation"].includes(location.pathname) || location.pathname.startsWith("/hosts/") ? "flex-1 max-w-none" : "max-w-sm"}`}
|
||||
className={`flex items-center ${["/", "/hosts", "/repositories", "/packages", "/automation", "/docker"].includes(location.pathname) || location.pathname.startsWith("/hosts/") || location.pathname.startsWith("/docker/") ? "flex-1 max-w-none" : "max-w-sm"}`}
|
||||
>
|
||||
<GlobalSearch />
|
||||
</div>
|
||||
|
1003
frontend/src/pages/Docker.jsx
Normal file
1003
frontend/src/pages/Docker.jsx
Normal file
File diff suppressed because it is too large
Load Diff
389
frontend/src/pages/docker/ContainerDetail.jsx
Normal file
389
frontend/src/pages/docker/ContainerDetail.jsx
Normal file
@@ -0,0 +1,389 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
Container,
|
||||
ExternalLink,
|
||||
RefreshCw,
|
||||
Server,
|
||||
} from "lucide-react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import api, { formatRelativeTime } from "../../utils/api";
|
||||
|
||||
const ContainerDetail = () => {
|
||||
const { id } = useParams();
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["docker", "container", id],
|
||||
queryFn: async () => {
|
||||
const response = await api.get(`/docker/containers/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const container = data?.container;
|
||||
const similarContainers = data?.similarContainers || [];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-secondary-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !container) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<div className="flex">
|
||||
<AlertTriangle className="h-5 w-5 text-red-400" />
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
|
||||
Container not found
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-red-700 dark:text-red-300">
|
||||
The container you're looking for doesn't exist or has been
|
||||
removed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to="/docker"
|
||||
className="mt-4 inline-flex items-center text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Docker
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const statusClasses = {
|
||||
running:
|
||||
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
||||
exited: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
|
||||
paused:
|
||||
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
|
||||
restarting:
|
||||
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
|
||||
};
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
|
||||
statusClasses[status] ||
|
||||
"bg-secondary-100 text-secondary-800 dark:bg-secondary-700 dark:text-secondary-200"
|
||||
}`}
|
||||
>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<Link
|
||||
to="/docker"
|
||||
className="inline-flex items-center text-sm text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Docker
|
||||
</Link>
|
||||
<div className="flex items-center">
|
||||
<Container className="h-8 w-8 text-secondary-400 mr-3" />
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 dark:text-white">
|
||||
{container.name}
|
||||
</h1>
|
||||
{getStatusBadge(container.status)}
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-secondary-600 dark:text-secondary-400">
|
||||
Container ID: {container.container_id.substring(0, 12)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview Cards */}
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{/* Update Status Card */}
|
||||
{container.docker_images?.docker_image_updates &&
|
||||
container.docker_images.docker_image_updates.length > 0 ? (
|
||||
<div className="card p-4 bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-400 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-yellow-200">
|
||||
Update Available
|
||||
</p>
|
||||
<p className="text-sm font-medium text-secondary-900 dark:text-yellow-100 truncate">
|
||||
{
|
||||
container.docker_images.docker_image_updates[0]
|
||||
.available_tag
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card p-4 bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-green-200">
|
||||
Update Status
|
||||
</p>
|
||||
<p className="text-sm font-medium text-secondary-900 dark:text-green-100">
|
||||
Up to date
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<Server className="h-5 w-5 text-purple-600 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">Host</p>
|
||||
<Link
|
||||
to={`/hosts/${container.host?.id}`}
|
||||
className="text-sm font-medium text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 truncate block"
|
||||
>
|
||||
{container.host?.friendly_name || container.host?.hostname}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<Container className="h-5 w-5 text-green-600 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">
|
||||
State
|
||||
</p>
|
||||
<p className="text-sm font-medium text-secondary-900 dark:text-white">
|
||||
{container.state || container.status}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<RefreshCw className="h-5 w-5 text-secondary-400 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">
|
||||
Last Checked
|
||||
</p>
|
||||
<p className="text-sm font-medium text-secondary-900 dark:text-white">
|
||||
{formatRelativeTime(container.last_checked)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Container and Image Information - Side by Side */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Container Details */}
|
||||
<div className="card">
|
||||
<div className="px-6 py-5 border-b border-secondary-200 dark:border-secondary-700">
|
||||
<h3 className="text-lg leading-6 font-medium text-secondary-900 dark:text-white">
|
||||
Container Information
|
||||
</h3>
|
||||
</div>
|
||||
<div className="px-6 py-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Container ID
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white font-mono break-all">
|
||||
{container.container_id}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Image Tag
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{container.image_tag}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Created
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{formatRelativeTime(container.created_at)}
|
||||
</dd>
|
||||
</div>
|
||||
{container.started_at && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Started
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{formatRelativeTime(container.started_at)}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{container.ports && Object.keys(container.ports).length > 0 && (
|
||||
<div className="sm:col-span-2">
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Port Mappings
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(container.ports).map(([key, value]) => (
|
||||
<span
|
||||
key={key}
|
||||
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||
>
|
||||
{key} → {value}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Information */}
|
||||
{container.docker_images && (
|
||||
<div className="card">
|
||||
<div className="px-6 py-5 border-b border-secondary-200 dark:border-secondary-700">
|
||||
<h3 className="text-lg leading-6 font-medium text-secondary-900 dark:text-white">
|
||||
Image Information
|
||||
</h3>
|
||||
</div>
|
||||
<div className="px-6 py-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Repository
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
<Link
|
||||
to={`/docker/images/${container.docker_images.id}`}
|
||||
className="text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 inline-flex items-center"
|
||||
>
|
||||
{container.docker_images.repository}
|
||||
<ExternalLink className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Tag
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{container.docker_images.tag}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Source
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{container.docker_images.source}
|
||||
</dd>
|
||||
</div>
|
||||
{container.docker_images.size_bytes && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Size
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{(
|
||||
Number(container.docker_images.size_bytes) /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2)}{" "}
|
||||
MB
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Image ID
|
||||
</dt>
|
||||
<dd className="mt-1 text-xs text-secondary-900 dark:text-white font-mono break-all">
|
||||
{container.docker_images.image_id?.substring(0, 12)}...
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Created
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{formatRelativeTime(container.docker_images.created_at)}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Similar Containers */}
|
||||
{similarContainers.length > 0 && (
|
||||
<div className="card">
|
||||
<div className="px-6 py-5 border-b border-secondary-200 dark:border-secondary-700">
|
||||
<h3 className="text-lg leading-6 font-medium text-secondary-900 dark:text-white">
|
||||
Similar Containers (Same Image)
|
||||
</h3>
|
||||
</div>
|
||||
<div className="px-6 py-5">
|
||||
<ul className="divide-y divide-secondary-200 dark:divide-secondary-700">
|
||||
{similarContainers.map((similar) => (
|
||||
<li
|
||||
key={similar.id}
|
||||
className="py-4 flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Container className="h-5 w-5 text-secondary-400 mr-3" />
|
||||
<div>
|
||||
<Link
|
||||
to={`/docker/containers/${similar.id}`}
|
||||
className="text-sm font-medium text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
{similar.name}
|
||||
</Link>
|
||||
<p className="text-sm text-secondary-500 dark:text-secondary-400">
|
||||
{similar.status}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContainerDetail;
|
354
frontend/src/pages/docker/HostDetail.jsx
Normal file
354
frontend/src/pages/docker/HostDetail.jsx
Normal file
@@ -0,0 +1,354 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
Container,
|
||||
ExternalLink,
|
||||
Package,
|
||||
RefreshCw,
|
||||
Server,
|
||||
} from "lucide-react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import api from "../../utils/api";
|
||||
|
||||
const HostDetail = () => {
|
||||
const { id } = useParams();
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["docker", "host", id],
|
||||
queryFn: async () => {
|
||||
const response = await api.get(`/docker/hosts/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const host = data?.host;
|
||||
const containers = data?.containers || [];
|
||||
const images = data?.images || [];
|
||||
const stats = data?.stats;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-secondary-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !host) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<div className="flex">
|
||||
<AlertTriangle className="h-5 w-5 text-red-400" />
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
|
||||
Host not found
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to="/docker"
|
||||
className="mt-4 inline-flex items-center text-primary-600 hover:text-primary-900"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Docker
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Link
|
||||
to="/docker"
|
||||
className="inline-flex items-center text-sm text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Docker
|
||||
</Link>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center">
|
||||
<Server className="h-8 w-8 text-secondary-400 mr-3" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-secondary-900 dark:text-white">
|
||||
{host.friendly_name || host.hostname}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-secondary-600 dark:text-secondary-400">
|
||||
{host.ip}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to={`/hosts/${id}`}
|
||||
className="inline-flex items-center text-sm text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
View Full Host Details
|
||||
<ExternalLink className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview Cards */}
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<Container className="h-5 w-5 text-blue-600 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">
|
||||
Total Containers
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{stats?.totalContainers || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<Container className="h-5 w-5 text-green-600 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">
|
||||
Running
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{stats?.runningContainers || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<Container 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">
|
||||
Stopped
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{stats?.stoppedContainers || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<Package className="h-5 w-5 text-purple-600 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">
|
||||
Images
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{stats?.totalImages || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Host Information */}
|
||||
<div className="card">
|
||||
<div className="px-6 py-5 border-b border-secondary-200 dark:border-secondary-700">
|
||||
<h3 className="text-lg leading-6 font-medium text-secondary-900 dark:text-white">
|
||||
Host Information
|
||||
</h3>
|
||||
</div>
|
||||
<div className="px-6 py-5 space-y-6">
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Friendly Name
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{host.friendly_name}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Hostname
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{host.hostname}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
IP Address
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{host.ip}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
OS
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{host.os_type} {host.os_version}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Containers */}
|
||||
<div className="card">
|
||||
<div className="px-6 py-5 border-b border-secondary-200 dark:border-secondary-700">
|
||||
<h3 className="text-lg leading-6 font-medium text-secondary-900 dark:text-white">
|
||||
Containers ({containers.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-secondary-200 dark:divide-secondary-700">
|
||||
<thead className="bg-secondary-50 dark:bg-secondary-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Container Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Image
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-right text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-700">
|
||||
{containers.map((container) => (
|
||||
<tr key={container.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Link
|
||||
to={`/docker/containers/${container.id}`}
|
||||
className="text-sm font-medium text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
{container.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500">
|
||||
{container.image_name}:{container.image_tag}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-secondary-100 text-secondary-800 dark:bg-secondary-700 dark:text-secondary-200">
|
||||
{container.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<Link
|
||||
to={`/docker/containers/${container.id}`}
|
||||
className="text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 inline-flex items-center"
|
||||
>
|
||||
View
|
||||
<ExternalLink className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Images */}
|
||||
<div className="card">
|
||||
<div className="px-6 py-5 border-b border-secondary-200 dark:border-secondary-700">
|
||||
<h3 className="text-lg leading-6 font-medium text-secondary-900 dark:text-white">
|
||||
Images ({images.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-secondary-200 dark:divide-secondary-700">
|
||||
<thead className="bg-secondary-50 dark:bg-secondary-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Repository
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Tag
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Source
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-right text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-700">
|
||||
{images.map((image) => (
|
||||
<tr key={image.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Link
|
||||
to={`/docker/images/${image.id}`}
|
||||
className="text-sm font-medium text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
{image.repository}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-secondary-100 text-secondary-800 dark:bg-secondary-700 dark:text-secondary-200">
|
||||
{image.tag}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500">
|
||||
{image.source}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<Link
|
||||
to={`/docker/images/${image.id}`}
|
||||
className="text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 inline-flex items-center"
|
||||
>
|
||||
View
|
||||
<ExternalLink className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HostDetail;
|
439
frontend/src/pages/docker/ImageDetail.jsx
Normal file
439
frontend/src/pages/docker/ImageDetail.jsx
Normal file
@@ -0,0 +1,439 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
Container,
|
||||
ExternalLink,
|
||||
Package,
|
||||
RefreshCw,
|
||||
Server,
|
||||
Shield,
|
||||
} from "lucide-react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import api, { formatRelativeTime } from "../../utils/api";
|
||||
|
||||
const ImageDetail = () => {
|
||||
const { id } = useParams();
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["docker", "image", id],
|
||||
queryFn: async () => {
|
||||
const response = await api.get(`/docker/images/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const image = data?.image;
|
||||
const hosts = data?.hosts || [];
|
||||
const containers = image?.docker_containers || [];
|
||||
const updates = image?.docker_image_updates || [];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-secondary-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !image) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<div className="flex">
|
||||
<AlertTriangle className="h-5 w-5 text-red-400" />
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
|
||||
Image not found
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to="/docker"
|
||||
className="mt-4 inline-flex items-center text-primary-600 hover:text-primary-900"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Docker
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Link
|
||||
to="/docker"
|
||||
className="inline-flex items-center text-sm text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Docker
|
||||
</Link>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center">
|
||||
<Package className="h-8 w-8 text-secondary-400 mr-3" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-secondary-900 dark:text-white">
|
||||
{image.repository}:{image.tag}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-secondary-600 dark:text-secondary-400">
|
||||
Image ID: {image.image_id.substring(0, 12)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview Cards */}
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<Container className="h-5 w-5 text-green-600 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">
|
||||
Containers
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{containers.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<Server className="h-5 w-5 text-purple-600 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">
|
||||
Hosts
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{hosts.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<Package className="h-5 w-5 text-blue-600 mr-2" />
|
||||
</div>
|
||||
<div className="w-0 flex-1">
|
||||
<p className="text-sm text-secondary-500 dark:text-white">Size</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{image.size_bytes ? (
|
||||
<>{(Number(image.size_bytes) / 1024 / 1024).toFixed(0)} MB</>
|
||||
) : (
|
||||
"N/A"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<AlertTriangle 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">
|
||||
Updates
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
{updates.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Available Updates with Digest Comparison */}
|
||||
{updates.length > 0 && (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
||||
<div className="flex">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-400" />
|
||||
<div className="ml-3 flex-1">
|
||||
<h3 className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
||||
Updates Available
|
||||
</h3>
|
||||
<div className="mt-2 space-y-3">
|
||||
{updates.map((update) => {
|
||||
let digestInfo = null;
|
||||
try {
|
||||
if (update.changelog_url) {
|
||||
digestInfo = JSON.parse(update.changelog_url);
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={update.id}
|
||||
className="bg-white dark:bg-secondary-800 rounded-lg p-3 border border-yellow-200 dark:border-yellow-700"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{update.is_security_update && (
|
||||
<Shield className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
<span className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
||||
New version available:{" "}
|
||||
<span className="font-semibold">
|
||||
{update.available_tag}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{update.is_security_update && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
|
||||
Security
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{digestInfo &&
|
||||
digestInfo.method === "digest_comparison" && (
|
||||
<div className="mt-2 pt-2 border-t border-yellow-200 dark:border-yellow-700">
|
||||
<p className="text-xs text-secondary-600 dark:text-secondary-400 mb-1">
|
||||
Detected via digest comparison:
|
||||
</p>
|
||||
<div className="font-mono text-xs space-y-1">
|
||||
<div className="text-red-600 dark:text-red-400">
|
||||
<span className="font-bold">- Current: </span>
|
||||
{digestInfo.current_digest}
|
||||
</div>
|
||||
<div className="text-green-600 dark:text-green-400">
|
||||
<span className="font-bold">+ Available: </span>
|
||||
{digestInfo.available_digest}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image Information */}
|
||||
<div className="card">
|
||||
<div className="px-6 py-5 border-b border-secondary-200 dark:border-secondary-700">
|
||||
<h3 className="text-lg leading-6 font-medium text-secondary-900 dark:text-white">
|
||||
Image Information
|
||||
</h3>
|
||||
</div>
|
||||
<div className="px-6 py-5 space-y-6">
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Repository
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{image.repository}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Tag
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{image.tag}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Source
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{image.source}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Created
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{image.created_at
|
||||
? formatRelativeTime(image.created_at)
|
||||
: "Unknown"}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Image ID
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm font-mono text-secondary-900 dark:text-white">
|
||||
{image.image_id}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Last Checked
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-secondary-900 dark:text-white">
|
||||
{image.last_checked
|
||||
? formatRelativeTime(image.last_checked)
|
||||
: "Never"}
|
||||
</dd>
|
||||
</div>
|
||||
{image.digest && (
|
||||
<div className="sm:col-span-2">
|
||||
<dt className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
|
||||
Digest
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm font-mono text-secondary-900 dark:text-white break-all">
|
||||
{image.digest}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Containers using this image */}
|
||||
<div className="card">
|
||||
<div className="px-6 py-5 border-b border-secondary-200 dark:border-secondary-700">
|
||||
<h3 className="text-lg leading-6 font-medium text-secondary-900 dark:text-white">
|
||||
Containers ({containers.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-secondary-200 dark:divide-secondary-700">
|
||||
<thead className="bg-secondary-50 dark:bg-secondary-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Container Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Host
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-right text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-700">
|
||||
{containers.map((container) => (
|
||||
<tr key={container.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Link
|
||||
to={`/docker/containers/${container.id}`}
|
||||
className="text-sm font-medium text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
{container.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-secondary-100 text-secondary-800 dark:bg-secondary-700 dark:text-secondary-200">
|
||||
{container.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500">
|
||||
{container.host_id}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<Link
|
||||
to={`/docker/containers/${container.id}`}
|
||||
className="text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 inline-flex items-center"
|
||||
>
|
||||
View
|
||||
<ExternalLink className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hosts using this image */}
|
||||
<div className="card">
|
||||
<div className="px-6 py-5 border-b border-secondary-200 dark:border-secondary-700">
|
||||
<h3 className="text-lg leading-6 font-medium text-secondary-900 dark:text-white">
|
||||
Hosts ({hosts.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-secondary-200 dark:divide-secondary-700">
|
||||
<thead className="bg-secondary-50 dark:bg-secondary-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Host Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
IP Address
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-right text-xs font-medium text-secondary-500 dark:text-secondary-400 uppercase tracking-wider"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-700">
|
||||
{hosts.map((host) => (
|
||||
<tr key={host.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Link
|
||||
to={`/hosts/${host.id}`}
|
||||
className="text-sm font-medium text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
{host.friendly_name || host.hostname}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500">
|
||||
{host.ip}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<Link
|
||||
to={`/hosts/${host.id}`}
|
||||
className="text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 inline-flex items-center"
|
||||
>
|
||||
View
|
||||
<ExternalLink className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageDetail;
|
@@ -2,6 +2,7 @@ import {
|
||||
AlertCircle,
|
||||
BookOpen,
|
||||
CheckCircle,
|
||||
Container,
|
||||
Copy,
|
||||
Eye,
|
||||
EyeOff,
|
||||
@@ -255,6 +256,17 @@ const Integrations = () => {
|
||||
>
|
||||
GetHomepage
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabChange("docker")}
|
||||
className={`px-6 py-3 text-sm font-medium ${
|
||||
activeTab === "docker"
|
||||
? "text-primary-600 dark:text-primary-400 border-b-2 border-primary-500 bg-primary-50 dark:bg-primary-900/20"
|
||||
: "text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-300 hover:bg-secondary-50 dark:hover:bg-secondary-700/50"
|
||||
}`}
|
||||
>
|
||||
Docker
|
||||
</button>
|
||||
{/* Future tabs can be added here */}
|
||||
</div>
|
||||
|
||||
@@ -723,6 +735,256 @@ const Integrations = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Docker Tab */}
|
||||
{activeTab === "docker" && (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-primary-100 dark:bg-primary-900 rounded-lg flex items-center justify-center">
|
||||
<Container className="h-5 w-5 text-primary-600 dark:text-primary-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
|
||||
Docker Container Monitoring
|
||||
</h3>
|
||||
<p className="text-sm text-secondary-600 dark:text-secondary-400">
|
||||
Monitor Docker containers and images for available updates
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Installation Instructions */}
|
||||
<div className="bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-primary-900 dark:text-primary-200 mb-4">
|
||||
Agent Installation
|
||||
</h3>
|
||||
<ol className="list-decimal list-inside space-y-3 text-sm text-primary-800 dark:text-primary-300">
|
||||
<li>
|
||||
Make sure you have the PatchMon credentials file set up on
|
||||
your host (
|
||||
<code className="bg-primary-100 dark:bg-primary-900/40 px-1 py-0.5 rounded text-xs">
|
||||
/etc/patchmon/credentials
|
||||
</code>
|
||||
)
|
||||
</li>
|
||||
<li>
|
||||
SSH into your Docker host where you want to monitor
|
||||
containers
|
||||
</li>
|
||||
<li>Run the installation command below</li>
|
||||
<li>
|
||||
The agent will automatically collect Docker container and
|
||||
image information every 5 minutes
|
||||
</li>
|
||||
<li>View your Docker inventory in the Docker page</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{/* Installation Command */}
|
||||
<div className="bg-white dark:bg-secondary-900 border border-secondary-200 dark:border-secondary-600 rounded-lg p-6">
|
||||
<h4 className="text-md font-semibold text-secondary-900 dark:text-white mb-3">
|
||||
Quick Installation (One-Line Command)
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
||||
Download and install the Docker agent:
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={`curl -o /usr/local/bin/patchmon-docker-agent.sh "${server_url}/api/v1/docker/agent" && chmod +x /usr/local/bin/patchmon-docker-agent.sh && echo "*/5 * * * * /usr/local/bin/patchmon-docker-agent.sh collect" | crontab -`}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-secondary-50 dark:bg-secondary-900 text-secondary-900 dark:text-white font-mono text-xs"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copy_to_clipboard(
|
||||
`curl -o /usr/local/bin/patchmon-docker-agent.sh "${server_url}/api/v1/docker/agent" && chmod +x /usr/local/bin/patchmon-docker-agent.sh && echo "*/5 * * * * /usr/local/bin/patchmon-docker-agent.sh collect" | crontab -`,
|
||||
"docker-install",
|
||||
)
|
||||
}
|
||||
className="btn-primary flex items-center gap-1 px-3 py-2 whitespace-nowrap"
|
||||
>
|
||||
{copy_success["docker-install"] ? (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-400 mt-2">
|
||||
💡 This will download the agent, make it executable, and
|
||||
set up a cron job to run every 5 minutes
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Manual Installation Steps */}
|
||||
<div className="bg-white dark:bg-secondary-900 border border-secondary-200 dark:border-secondary-600 rounded-lg p-6">
|
||||
<h4 className="text-md font-semibold text-secondary-900 dark:text-white mb-3">
|
||||
Manual Installation Steps
|
||||
</h4>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-secondary-700 dark:text-secondary-300 mb-2">
|
||||
<strong>Step 1:</strong> Download the agent
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={`curl -o /usr/local/bin/patchmon-docker-agent.sh "${server_url}/api/v1/docker/agent"`}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-secondary-50 dark:bg-secondary-900 text-secondary-900 dark:text-white font-mono text-xs"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copy_to_clipboard(
|
||||
`curl -o /usr/local/bin/patchmon-docker-agent.sh "${server_url}/api/v1/docker/agent"`,
|
||||
"docker-download",
|
||||
)
|
||||
}
|
||||
className="btn-primary p-2"
|
||||
>
|
||||
{copy_success["docker-download"] ? (
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-secondary-700 dark:text-secondary-300 mb-2">
|
||||
<strong>Step 2:</strong> Make it executable
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value="chmod +x /usr/local/bin/patchmon-docker-agent.sh"
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-secondary-50 dark:bg-secondary-900 text-secondary-900 dark:text-white font-mono text-xs"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copy_to_clipboard(
|
||||
"chmod +x /usr/local/bin/patchmon-docker-agent.sh",
|
||||
"docker-chmod",
|
||||
)
|
||||
}
|
||||
className="btn-primary p-2"
|
||||
>
|
||||
{copy_success["docker-chmod"] ? (
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-secondary-700 dark:text-secondary-300 mb-2">
|
||||
<strong>Step 3:</strong> Test the agent
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value="/usr/local/bin/patchmon-docker-agent.sh collect"
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-secondary-50 dark:bg-secondary-900 text-secondary-900 dark:text-white font-mono text-xs"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copy_to_clipboard(
|
||||
"/usr/local/bin/patchmon-docker-agent.sh collect",
|
||||
"docker-test",
|
||||
)
|
||||
}
|
||||
className="btn-primary p-2"
|
||||
>
|
||||
{copy_success["docker-test"] ? (
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-secondary-700 dark:text-secondary-300 mb-2">
|
||||
<strong>Step 4:</strong> Set up automatic collection
|
||||
(every 5 minutes)
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value='echo "*/5 * * * * /usr/local/bin/patchmon-docker-agent.sh collect" | crontab -'
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-secondary-50 dark:bg-secondary-900 text-secondary-900 dark:text-white font-mono text-xs"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copy_to_clipboard(
|
||||
'echo "*/5 * * * * /usr/local/bin/patchmon-docker-agent.sh collect" | crontab -',
|
||||
"docker-cron",
|
||||
)
|
||||
}
|
||||
className="btn-primary p-2"
|
||||
>
|
||||
{copy_success["docker-cron"] ? (
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prerequisites */}
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<p className="font-semibold mb-2">Prerequisites:</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li>
|
||||
Docker must be installed and running on the host
|
||||
</li>
|
||||
<li>
|
||||
PatchMon credentials file must exist at{" "}
|
||||
<code className="bg-yellow-100 dark:bg-yellow-900/40 px-1 py-0.5 rounded text-xs">
|
||||
/etc/patchmon/credentials
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
The host must have network access to your PatchMon
|
||||
server
|
||||
</li>
|
||||
<li>The agent must run as root (or with sudo)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user