mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-11-04 05:53:27 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			2126 lines
		
	
	
		
			67 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2126 lines
		
	
	
		
			67 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
 | 
						|
import {
 | 
						|
	AlertTriangle,
 | 
						|
	ArrowDown,
 | 
						|
	ArrowUp,
 | 
						|
	ArrowUpDown,
 | 
						|
	CheckCircle,
 | 
						|
	CheckSquare,
 | 
						|
	ChevronDown,
 | 
						|
	Clock,
 | 
						|
	Columns,
 | 
						|
	ExternalLink,
 | 
						|
	Eye as EyeIcon,
 | 
						|
	EyeOff as EyeOffIcon,
 | 
						|
	Filter,
 | 
						|
	GripVertical,
 | 
						|
	Plus,
 | 
						|
	RefreshCw,
 | 
						|
	Search,
 | 
						|
	Server,
 | 
						|
	Square,
 | 
						|
	Trash2,
 | 
						|
	Users,
 | 
						|
	Wifi,
 | 
						|
	X,
 | 
						|
} from "lucide-react";
 | 
						|
import { useEffect, useId, useMemo, useState } from "react";
 | 
						|
import { Link, useNavigate, useSearchParams } from "react-router-dom";
 | 
						|
import InlineEdit from "../components/InlineEdit";
 | 
						|
import InlineMultiGroupEdit from "../components/InlineMultiGroupEdit";
 | 
						|
import InlineToggle from "../components/InlineToggle";
 | 
						|
import {
 | 
						|
	adminHostsAPI,
 | 
						|
	dashboardAPI,
 | 
						|
	formatRelativeTime,
 | 
						|
	hostGroupsAPI,
 | 
						|
} from "../utils/api";
 | 
						|
import { getOSDisplayName, OSIcon } from "../utils/osIcons.jsx";
 | 
						|
 | 
						|
// Add Host Modal Component
 | 
						|
const AddHostModal = ({ isOpen, onClose, onSuccess }) => {
 | 
						|
	const friendlyNameId = useId();
 | 
						|
	const [formData, setFormData] = useState({
 | 
						|
		friendly_name: "",
 | 
						|
		hostGroupIds: [], // Changed to array for multiple selection
 | 
						|
	});
 | 
						|
	const [isSubmitting, setIsSubmitting] = useState(false);
 | 
						|
	const [error, setError] = useState("");
 | 
						|
 | 
						|
	// Fetch host groups for selection
 | 
						|
	const { data: hostGroups } = useQuery({
 | 
						|
		queryKey: ["hostGroups"],
 | 
						|
		queryFn: () => hostGroupsAPI.list().then((res) => res.data),
 | 
						|
		enabled: isOpen,
 | 
						|
	});
 | 
						|
 | 
						|
	const handleSubmit = async (e) => {
 | 
						|
		e.preventDefault();
 | 
						|
		setIsSubmitting(true);
 | 
						|
		setError("");
 | 
						|
 | 
						|
		console.log("Creating host:", formData.friendly_name);
 | 
						|
 | 
						|
		try {
 | 
						|
			const response = await adminHostsAPI.create(formData);
 | 
						|
			console.log("Host created successfully:", formData.friendly_name);
 | 
						|
			onSuccess(response.data);
 | 
						|
			setFormData({ friendly_name: "", hostGroupIds: [] });
 | 
						|
			onClose();
 | 
						|
		} catch (err) {
 | 
						|
			console.error("Full error object:", err);
 | 
						|
			console.error("Error response:", err.response);
 | 
						|
 | 
						|
			let errorMessage = "Failed to create host";
 | 
						|
 | 
						|
			if (err.response?.data?.errors) {
 | 
						|
				// Validation errors
 | 
						|
				errorMessage = err.response.data.errors.map((e) => e.msg).join(", ");
 | 
						|
			} else if (err.response?.data?.error) {
 | 
						|
				// Single error message
 | 
						|
				errorMessage = err.response.data.error;
 | 
						|
			} else if (err.message) {
 | 
						|
				// Network or other error
 | 
						|
				errorMessage = err.message;
 | 
						|
			}
 | 
						|
 | 
						|
			setError(errorMessage);
 | 
						|
		} finally {
 | 
						|
			setIsSubmitting(false);
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	if (!isOpen) return null;
 | 
						|
 | 
						|
	return (
 | 
						|
		<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
 | 
						|
			<div className="bg-white dark:bg-secondary-800 rounded-lg p-6 w-full max-w-md">
 | 
						|
				<div className="flex justify-between items-center mb-4">
 | 
						|
					<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
 | 
						|
						Add New Host
 | 
						|
					</h3>
 | 
						|
					<button
 | 
						|
						type="button"
 | 
						|
						onClick={onClose}
 | 
						|
						className="text-secondary-400 hover:text-secondary-600 dark:text-secondary-500 dark:hover:text-secondary-300"
 | 
						|
					>
 | 
						|
						<X className="h-5 w-5" />
 | 
						|
					</button>
 | 
						|
				</div>
 | 
						|
 | 
						|
				<form onSubmit={handleSubmit} className="space-y-6">
 | 
						|
					<div>
 | 
						|
						<label
 | 
						|
							htmlFor={friendlyNameId}
 | 
						|
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
 | 
						|
						>
 | 
						|
							Friendly Name *
 | 
						|
						</label>
 | 
						|
						<input
 | 
						|
							type="text"
 | 
						|
							id={friendlyNameId}
 | 
						|
							required
 | 
						|
							value={formData.friendly_name}
 | 
						|
							onChange={(e) =>
 | 
						|
								setFormData({ ...formData, friendly_name: e.target.value })
 | 
						|
							}
 | 
						|
							className="block w-full px-3 py-2.5 text-base border-2 border-secondary-300 dark:border-secondary-600 rounded-lg shadow-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white transition-all duration-200"
 | 
						|
							placeholder="server.example.com"
 | 
						|
						/>
 | 
						|
						<p className="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
 | 
						|
							System information (OS, IP, architecture) will be automatically
 | 
						|
							detected when the agent connects.
 | 
						|
						</p>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div>
 | 
						|
						<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-3">
 | 
						|
							Host Groups
 | 
						|
						</span>
 | 
						|
						<div className="space-y-2 max-h-48 overflow-y-auto">
 | 
						|
							{/* Host Group Options */}
 | 
						|
							{hostGroups?.map((group) => (
 | 
						|
								<label
 | 
						|
									key={group.id}
 | 
						|
									className={`flex items-center gap-3 p-3 border-2 rounded-lg transition-all duration-200 cursor-pointer ${
 | 
						|
										formData.hostGroupIds.includes(group.id)
 | 
						|
											? "border-primary-500 bg-primary-50 dark:bg-primary-900/30"
 | 
						|
											: "border-secondary-300 dark:border-secondary-600 bg-white dark:bg-secondary-700 hover:border-secondary-400 dark:hover:border-secondary-500"
 | 
						|
									}`}
 | 
						|
								>
 | 
						|
									<input
 | 
						|
										type="checkbox"
 | 
						|
										checked={formData.hostGroupIds.includes(group.id)}
 | 
						|
										onChange={(e) => {
 | 
						|
											if (e.target.checked) {
 | 
						|
												setFormData({
 | 
						|
													...formData,
 | 
						|
													hostGroupIds: [...formData.hostGroupIds, group.id],
 | 
						|
												});
 | 
						|
											} else {
 | 
						|
												setFormData({
 | 
						|
													...formData,
 | 
						|
													hostGroupIds: formData.hostGroupIds.filter(
 | 
						|
														(id) => id !== group.id,
 | 
						|
													),
 | 
						|
												});
 | 
						|
											}
 | 
						|
										}}
 | 
						|
										className="w-4 h-4 text-primary-600 bg-gray-100 border-gray-300 rounded focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
 | 
						|
									/>
 | 
						|
									<div className="flex items-center gap-2 flex-1">
 | 
						|
										{group.color && (
 | 
						|
											<div
 | 
						|
												className="w-3 h-3 rounded-full border border-secondary-300 dark:border-secondary-500 flex-shrink-0"
 | 
						|
												style={{ backgroundColor: group.color }}
 | 
						|
											></div>
 | 
						|
										)}
 | 
						|
										<div className="text-sm font-medium text-secondary-700 dark:text-secondary-200">
 | 
						|
											{group.name}
 | 
						|
										</div>
 | 
						|
									</div>
 | 
						|
								</label>
 | 
						|
							))}
 | 
						|
						</div>
 | 
						|
						<p className="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
 | 
						|
							Optional: Select one or more groups to assign this host to for
 | 
						|
							better organization.
 | 
						|
						</p>
 | 
						|
					</div>
 | 
						|
 | 
						|
					{error && (
 | 
						|
						<div className="bg-danger-50 dark:bg-danger-900 border border-danger-200 dark:border-danger-700 rounded-md p-3">
 | 
						|
							<p className="text-sm text-danger-700 dark:text-danger-300">
 | 
						|
								{error}
 | 
						|
							</p>
 | 
						|
						</div>
 | 
						|
					)}
 | 
						|
 | 
						|
					<div className="flex justify-end space-x-3 pt-2">
 | 
						|
						<button
 | 
						|
							type="button"
 | 
						|
							onClick={onClose}
 | 
						|
							className="px-6 py-3 text-sm font-medium text-secondary-700 dark:text-secondary-200 bg-white dark:bg-secondary-700 border-2 border-secondary-300 dark:border-secondary-600 rounded-lg hover:bg-secondary-50 dark:hover:bg-secondary-600 transition-all duration-200"
 | 
						|
						>
 | 
						|
							Cancel
 | 
						|
						</button>
 | 
						|
						<button
 | 
						|
							type="submit"
 | 
						|
							disabled={isSubmitting}
 | 
						|
							className="px-6 py-3 text-sm font-medium text-white bg-primary-600 border-2 border-transparent rounded-lg hover:bg-primary-700 disabled:opacity-50 transition-all duration-200"
 | 
						|
						>
 | 
						|
							{isSubmitting ? "Creating..." : "Create Host"}
 | 
						|
						</button>
 | 
						|
					</div>
 | 
						|
				</form>
 | 
						|
			</div>
 | 
						|
		</div>
 | 
						|
	);
 | 
						|
};
 | 
						|
 | 
						|
const Hosts = () => {
 | 
						|
	const hostGroupFilterId = useId();
 | 
						|
	const statusFilterId = useId();
 | 
						|
	const osFilterId = useId();
 | 
						|
	const [showAddModal, setShowAddModal] = useState(false);
 | 
						|
	const [selectedHosts, setSelectedHosts] = useState([]);
 | 
						|
	const [showBulkAssignModal, setShowBulkAssignModal] = useState(false);
 | 
						|
	const [showBulkDeleteModal, setShowBulkDeleteModal] = useState(false);
 | 
						|
	const [searchParams] = useSearchParams();
 | 
						|
	const navigate = useNavigate();
 | 
						|
 | 
						|
	// Table state
 | 
						|
	const [searchTerm, setSearchTerm] = useState("");
 | 
						|
	const [sortField, setSortField] = useState("hostname");
 | 
						|
	const [sortDirection, setSortDirection] = useState("asc");
 | 
						|
	const [groupFilter, setGroupFilter] = useState("all");
 | 
						|
	const [statusFilter, setStatusFilter] = useState("all");
 | 
						|
	const [osFilter, setOsFilter] = useState("all");
 | 
						|
	const [showFilters, setShowFilters] = useState(false);
 | 
						|
	const [groupBy, setGroupBy] = useState("none");
 | 
						|
	const [showColumnSettings, setShowColumnSettings] = useState(false);
 | 
						|
	const [hideStale, setHideStale] = useState(false);
 | 
						|
 | 
						|
	// Handle URL filter parameters
 | 
						|
	useEffect(() => {
 | 
						|
		const filter = searchParams.get("filter");
 | 
						|
		const showFiltersParam = searchParams.get("showFilters");
 | 
						|
		const osFilterParam = searchParams.get("osFilter");
 | 
						|
		const groupParam = searchParams.get("group");
 | 
						|
 | 
						|
		if (filter === "needsUpdates") {
 | 
						|
			setShowFilters(true);
 | 
						|
			setStatusFilter("all");
 | 
						|
			// We'll filter hosts with updates > 0 in the filtering logic
 | 
						|
		} else if (filter === "inactive") {
 | 
						|
			setShowFilters(true);
 | 
						|
			setStatusFilter("inactive");
 | 
						|
			// We'll filter hosts with inactive status in the filtering logic
 | 
						|
		} else if (filter === "upToDate") {
 | 
						|
			setShowFilters(true);
 | 
						|
			setStatusFilter("active");
 | 
						|
			// We'll filter hosts that are up to date in the filtering logic
 | 
						|
		} else if (filter === "stale") {
 | 
						|
			setShowFilters(true);
 | 
						|
			setStatusFilter("all");
 | 
						|
			// We'll filter hosts that are stale in the filtering logic
 | 
						|
		} else if (showFiltersParam === "true") {
 | 
						|
			setShowFilters(true);
 | 
						|
		}
 | 
						|
 | 
						|
		// Handle OS filter parameter
 | 
						|
		if (osFilterParam) {
 | 
						|
			setShowFilters(true);
 | 
						|
			setOsFilter(osFilterParam);
 | 
						|
		}
 | 
						|
 | 
						|
		// Handle group filter parameter
 | 
						|
		if (groupParam) {
 | 
						|
			setShowFilters(true);
 | 
						|
			setGroupFilter(groupParam);
 | 
						|
		}
 | 
						|
 | 
						|
		// Handle add host action from navigation
 | 
						|
		const action = searchParams.get("action");
 | 
						|
		if (action === "add") {
 | 
						|
			setShowAddModal(true);
 | 
						|
			// Remove the action parameter from URL without triggering a page reload
 | 
						|
			const newSearchParams = new URLSearchParams(searchParams);
 | 
						|
			newSearchParams.delete("action");
 | 
						|
			navigate(
 | 
						|
				`/hosts${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ""}`,
 | 
						|
				{
 | 
						|
					replace: true,
 | 
						|
				},
 | 
						|
			);
 | 
						|
		}
 | 
						|
 | 
						|
		// Handle selected hosts from packages page
 | 
						|
		const selected = searchParams.get("selected");
 | 
						|
		if (selected) {
 | 
						|
			const hostIds = selected.split(",").filter(Boolean);
 | 
						|
			setSelectedHosts(hostIds);
 | 
						|
			// Remove the selected parameter from URL without triggering a page reload
 | 
						|
			const newSearchParams = new URLSearchParams(searchParams);
 | 
						|
			newSearchParams.delete("selected");
 | 
						|
			navigate(
 | 
						|
				`/hosts${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ""}`,
 | 
						|
				{
 | 
						|
					replace: true,
 | 
						|
				},
 | 
						|
			);
 | 
						|
		}
 | 
						|
	}, [searchParams, navigate]);
 | 
						|
 | 
						|
	// Column configuration
 | 
						|
	const [columnConfig, setColumnConfig] = useState(() => {
 | 
						|
		const defaultConfig = [
 | 
						|
			{ id: "select", label: "Select", visible: true, order: 0 },
 | 
						|
			{ id: "host", label: "Friendly Name", visible: true, order: 1 },
 | 
						|
			{ id: "hostname", label: "System Hostname", visible: true, order: 2 },
 | 
						|
			{ id: "ip", label: "IP Address", visible: false, order: 3 },
 | 
						|
			{ id: "group", label: "Group", visible: true, order: 4 },
 | 
						|
			{ id: "os", label: "OS", visible: true, order: 5 },
 | 
						|
			{ id: "os_version", label: "OS Version", visible: false, order: 6 },
 | 
						|
			{ id: "agent_version", label: "Agent Version", visible: true, order: 7 },
 | 
						|
			{
 | 
						|
				id: "auto_update",
 | 
						|
				label: "Agent Auto-Update",
 | 
						|
				visible: true,
 | 
						|
				order: 8,
 | 
						|
			},
 | 
						|
			{ id: "ws_status", label: "Connection", visible: true, order: 9 },
 | 
						|
			{ id: "status", label: "Status", visible: true, order: 10 },
 | 
						|
			{ id: "updates", label: "Updates", visible: true, order: 11 },
 | 
						|
			{ id: "notes", label: "Notes", visible: false, order: 12 },
 | 
						|
			{ id: "last_update", label: "Last Update", visible: true, order: 13 },
 | 
						|
			{ id: "actions", label: "Actions", visible: true, order: 14 },
 | 
						|
		];
 | 
						|
 | 
						|
		const saved = localStorage.getItem("hosts-column-config");
 | 
						|
		if (saved) {
 | 
						|
			try {
 | 
						|
				const savedConfig = JSON.parse(saved);
 | 
						|
 | 
						|
				// Check if we have old camelCase column IDs that need to be migrated
 | 
						|
				const hasOldColumns = savedConfig.some(
 | 
						|
					(col) =>
 | 
						|
						col.id === "agentVersion" ||
 | 
						|
						col.id === "autoUpdate" ||
 | 
						|
						col.id === "osVersion" ||
 | 
						|
						col.id === "lastUpdate",
 | 
						|
				);
 | 
						|
 | 
						|
				if (hasOldColumns) {
 | 
						|
					// Clear the old configuration and use the default snake_case configuration
 | 
						|
					localStorage.removeItem("hosts-column-config");
 | 
						|
					return defaultConfig;
 | 
						|
				} else {
 | 
						|
					// Ensure ws_status column is visible in saved config
 | 
						|
					const updatedConfig = savedConfig.map((col) =>
 | 
						|
						col.id === "ws_status" ? { ...col, visible: true } : col,
 | 
						|
					);
 | 
						|
					return updatedConfig;
 | 
						|
				}
 | 
						|
			} catch {
 | 
						|
				// If there's an error parsing the config, clear it and use default
 | 
						|
				localStorage.removeItem("hosts-column-config");
 | 
						|
				return defaultConfig;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return defaultConfig;
 | 
						|
	});
 | 
						|
 | 
						|
	const queryClient = useQueryClient();
 | 
						|
 | 
						|
	const {
 | 
						|
		data: hosts,
 | 
						|
		isLoading,
 | 
						|
		error,
 | 
						|
		refetch,
 | 
						|
		isFetching,
 | 
						|
	} = useQuery({
 | 
						|
		queryKey: ["hosts"],
 | 
						|
		queryFn: () => dashboardAPI.getHosts().then((res) => res.data),
 | 
						|
		staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
 | 
						|
		refetchOnWindowFocus: false, // Don't refetch when window regains focus
 | 
						|
	});
 | 
						|
 | 
						|
	const { data: hostGroups } = useQuery({
 | 
						|
		queryKey: ["hostGroups"],
 | 
						|
		queryFn: () => hostGroupsAPI.list().then((res) => res.data),
 | 
						|
	});
 | 
						|
 | 
						|
	// Track WebSocket status for all hosts
 | 
						|
	const [wsStatusMap, setWsStatusMap] = useState({});
 | 
						|
 | 
						|
	// Fetch initial WebSocket status for all hosts
 | 
						|
	useEffect(() => {
 | 
						|
		if (!hosts || hosts.length === 0) return;
 | 
						|
 | 
						|
		const token = localStorage.getItem("token");
 | 
						|
		if (!token) return;
 | 
						|
 | 
						|
		// Fetch initial WebSocket status for all hosts
 | 
						|
		// Fetch initial WebSocket status for all hosts
 | 
						|
		const fetchInitialStatus = async () => {
 | 
						|
			const apiIds = hosts
 | 
						|
				.filter((host) => host.api_id)
 | 
						|
				.map((host) => host.api_id);
 | 
						|
 | 
						|
			if (apiIds.length === 0) return;
 | 
						|
 | 
						|
			try {
 | 
						|
				const response = await fetch(
 | 
						|
					`/api/v1/ws/status?apiIds=${apiIds.join(",")}`,
 | 
						|
					{
 | 
						|
						headers: {
 | 
						|
							Authorization: `Bearer ${token}`,
 | 
						|
						},
 | 
						|
					},
 | 
						|
				);
 | 
						|
				if (response.ok) {
 | 
						|
					const result = await response.json();
 | 
						|
					setWsStatusMap(result.data);
 | 
						|
				}
 | 
						|
			} catch (_error) {
 | 
						|
				// Silently handle errors
 | 
						|
			}
 | 
						|
		};
 | 
						|
 | 
						|
		fetchInitialStatus();
 | 
						|
	}, [hosts]);
 | 
						|
 | 
						|
	// Subscribe to WebSocket status changes for all hosts via polling (lightweight alternative to SSE)
 | 
						|
	useEffect(() => {
 | 
						|
		if (!hosts || hosts.length === 0) return;
 | 
						|
 | 
						|
		const token = localStorage.getItem("token");
 | 
						|
		if (!token) return;
 | 
						|
 | 
						|
		// Use polling instead of SSE to avoid connection pool issues
 | 
						|
		// Poll every 10 seconds instead of 19 persistent connections
 | 
						|
		const pollInterval = setInterval(() => {
 | 
						|
			const apiIds = hosts
 | 
						|
				.filter((host) => host.api_id)
 | 
						|
				.map((host) => host.api_id);
 | 
						|
 | 
						|
			if (apiIds.length === 0) return;
 | 
						|
 | 
						|
			fetch(`/api/v1/ws/status?apiIds=${apiIds.join(",")}`, {
 | 
						|
				headers: {
 | 
						|
					Authorization: `Bearer ${token}`,
 | 
						|
				},
 | 
						|
			})
 | 
						|
				.then((response) => response.json())
 | 
						|
				.then((result) => {
 | 
						|
					if (result.success && result.data) {
 | 
						|
						setWsStatusMap(result.data);
 | 
						|
					}
 | 
						|
				})
 | 
						|
				.catch(() => {
 | 
						|
					// Silently handle errors
 | 
						|
				});
 | 
						|
		}, 10000); // Poll every 10 seconds
 | 
						|
 | 
						|
		// Cleanup function
 | 
						|
		return () => {
 | 
						|
			clearInterval(pollInterval);
 | 
						|
		};
 | 
						|
	}, [hosts]);
 | 
						|
 | 
						|
	const bulkUpdateGroupMutation = useMutation({
 | 
						|
		mutationFn: ({ hostIds, groupIds }) =>
 | 
						|
			adminHostsAPI.bulkUpdateGroups(hostIds, groupIds),
 | 
						|
		onSuccess: (data) => {
 | 
						|
			console.log("bulkUpdateGroupMutation success:", data);
 | 
						|
 | 
						|
			// Update the cache with the new host data
 | 
						|
			if (data?.hosts) {
 | 
						|
				queryClient.setQueryData(["hosts"], (oldData) => {
 | 
						|
					if (!oldData) return oldData;
 | 
						|
					return oldData.map((host) => {
 | 
						|
						const updatedHost = data.hosts.find((h) => h.id === host.id);
 | 
						|
						if (updatedHost) {
 | 
						|
							return updatedHost;
 | 
						|
						}
 | 
						|
						return host;
 | 
						|
					});
 | 
						|
				});
 | 
						|
			}
 | 
						|
 | 
						|
			// Also invalidate to ensure consistency
 | 
						|
			queryClient.invalidateQueries(["hosts"]);
 | 
						|
			setSelectedHosts([]);
 | 
						|
			setShowBulkAssignModal(false);
 | 
						|
		},
 | 
						|
	});
 | 
						|
 | 
						|
	const updateFriendlyNameMutation = useMutation({
 | 
						|
		mutationFn: ({ hostId, friendlyName }) =>
 | 
						|
			adminHostsAPI
 | 
						|
				.updateFriendlyName(hostId, friendlyName)
 | 
						|
				.then((res) => res.data),
 | 
						|
		onSuccess: () => {
 | 
						|
			queryClient.invalidateQueries(["hosts"]);
 | 
						|
		},
 | 
						|
	});
 | 
						|
 | 
						|
	const _updateHostGroupMutation = useMutation({
 | 
						|
		mutationFn: ({ hostId, hostGroupId }) => {
 | 
						|
			console.log("updateHostGroupMutation called with:", {
 | 
						|
				hostId,
 | 
						|
				hostGroupId,
 | 
						|
			});
 | 
						|
			return adminHostsAPI.updateGroup(hostId, hostGroupId).then((res) => {
 | 
						|
				console.log("updateGroup API response:", res);
 | 
						|
				return res.data;
 | 
						|
			});
 | 
						|
		},
 | 
						|
		onSuccess: (data) => {
 | 
						|
			// Update the cache with the new host data
 | 
						|
			queryClient.setQueryData(["hosts"], (oldData) => {
 | 
						|
				console.log("Old cache data before update:", oldData);
 | 
						|
				if (!oldData) return oldData;
 | 
						|
				const updatedData = oldData.map((host) => {
 | 
						|
					if (host.id === data.host.id) {
 | 
						|
						console.log(
 | 
						|
							"Updating host in cache:",
 | 
						|
							host.id,
 | 
						|
							"with new data:",
 | 
						|
							data.host,
 | 
						|
						);
 | 
						|
						// Host already has host_group_memberships from backend
 | 
						|
						const updatedHost = {
 | 
						|
							...data.host,
 | 
						|
						};
 | 
						|
						console.log("Updated host in cache:", updatedHost);
 | 
						|
						return updatedHost;
 | 
						|
					}
 | 
						|
					return host;
 | 
						|
				});
 | 
						|
				console.log("New cache data after update:", updatedData);
 | 
						|
				return updatedData;
 | 
						|
			});
 | 
						|
 | 
						|
			// Also invalidate to ensure consistency
 | 
						|
			queryClient.invalidateQueries(["hosts"]);
 | 
						|
		},
 | 
						|
		onError: (error) => {
 | 
						|
			console.error("updateHostGroupMutation error:", error);
 | 
						|
		},
 | 
						|
	});
 | 
						|
 | 
						|
	const updateHostGroupsMutation = useMutation({
 | 
						|
		mutationFn: ({ hostId, groupIds }) => {
 | 
						|
			console.log("updateHostGroupsMutation called with:", {
 | 
						|
				hostId,
 | 
						|
				groupIds,
 | 
						|
			});
 | 
						|
			return adminHostsAPI.updateGroups(hostId, groupIds).then((res) => {
 | 
						|
				console.log("updateGroups API response:", res);
 | 
						|
				return res.data;
 | 
						|
			});
 | 
						|
		},
 | 
						|
		onSuccess: (data) => {
 | 
						|
			// Update the cache with the new host data
 | 
						|
			queryClient.setQueryData(["hosts"], (oldData) => {
 | 
						|
				console.log("Old cache data before update:", oldData);
 | 
						|
				if (!oldData) return oldData;
 | 
						|
				const updatedData = oldData.map((host) => {
 | 
						|
					if (host.id === data.host.id) {
 | 
						|
						console.log(
 | 
						|
							"Updating host in cache:",
 | 
						|
							host.id,
 | 
						|
							"with new data:",
 | 
						|
							data.host,
 | 
						|
						);
 | 
						|
						return data.host;
 | 
						|
					}
 | 
						|
					return host;
 | 
						|
				});
 | 
						|
				console.log("New cache data after update:", updatedData);
 | 
						|
				return updatedData;
 | 
						|
			});
 | 
						|
 | 
						|
			// Also invalidate to ensure consistency
 | 
						|
			queryClient.invalidateQueries(["hosts"]);
 | 
						|
		},
 | 
						|
		onError: (error) => {
 | 
						|
			console.error("updateHostGroupsMutation error:", error);
 | 
						|
		},
 | 
						|
	});
 | 
						|
 | 
						|
	const toggleAutoUpdateMutation = useMutation({
 | 
						|
		mutationFn: ({ hostId, autoUpdate }) =>
 | 
						|
			adminHostsAPI
 | 
						|
				.toggleAutoUpdate(hostId, autoUpdate)
 | 
						|
				.then((res) => res.data),
 | 
						|
		onSuccess: () => {
 | 
						|
			queryClient.invalidateQueries(["hosts"]);
 | 
						|
		},
 | 
						|
	});
 | 
						|
 | 
						|
	const bulkDeleteMutation = useMutation({
 | 
						|
		mutationFn: (hostIds) => adminHostsAPI.deleteBulk(hostIds),
 | 
						|
		onSuccess: (data) => {
 | 
						|
			console.log("Bulk delete success:", data);
 | 
						|
			queryClient.invalidateQueries(["hosts"]);
 | 
						|
			setSelectedHosts([]);
 | 
						|
			setShowBulkDeleteModal(false);
 | 
						|
		},
 | 
						|
		onError: (error) => {
 | 
						|
			console.error("Bulk delete error:", error);
 | 
						|
		},
 | 
						|
	});
 | 
						|
 | 
						|
	// Helper functions for bulk selection
 | 
						|
	const handleSelectHost = (hostId) => {
 | 
						|
		setSelectedHosts((prev) =>
 | 
						|
			prev.includes(hostId)
 | 
						|
				? prev.filter((id) => id !== hostId)
 | 
						|
				: [...prev, hostId],
 | 
						|
		);
 | 
						|
	};
 | 
						|
 | 
						|
	const handleSelectAll = () => {
 | 
						|
		if (selectedHosts.length === hosts.length) {
 | 
						|
			setSelectedHosts([]);
 | 
						|
		} else {
 | 
						|
			setSelectedHosts(hosts.map((host) => host.id));
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	const handleBulkAssign = (groupIds) => {
 | 
						|
		bulkUpdateGroupMutation.mutate({ hostIds: selectedHosts, groupIds });
 | 
						|
	};
 | 
						|
 | 
						|
	const handleBulkDelete = () => {
 | 
						|
		bulkDeleteMutation.mutate(selectedHosts);
 | 
						|
	};
 | 
						|
 | 
						|
	// Table filtering and sorting logic
 | 
						|
	const filteredAndSortedHosts = useMemo(() => {
 | 
						|
		if (!hosts) return [];
 | 
						|
 | 
						|
		const filtered = hosts.filter((host) => {
 | 
						|
			// Search filter
 | 
						|
			const matchesSearch =
 | 
						|
				searchTerm === "" ||
 | 
						|
				host.friendly_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
						|
				host.ip?.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
						|
				host.os_type?.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
						|
				host.notes?.toLowerCase().includes(searchTerm.toLowerCase());
 | 
						|
 | 
						|
			// Group filter - handle multiple groups per host
 | 
						|
			const memberships = host.host_group_memberships || [];
 | 
						|
			const matchesGroup =
 | 
						|
				groupFilter === "all" ||
 | 
						|
				(groupFilter === "ungrouped" && memberships.length === 0) ||
 | 
						|
				(groupFilter !== "ungrouped" &&
 | 
						|
					memberships.some(
 | 
						|
						(membership) => membership.host_groups?.id === groupFilter,
 | 
						|
					));
 | 
						|
 | 
						|
			// Status filter
 | 
						|
			const matchesStatus =
 | 
						|
				statusFilter === "all" ||
 | 
						|
				(host.effectiveStatus || host.status) === statusFilter;
 | 
						|
 | 
						|
			// OS filter
 | 
						|
			const matchesOs =
 | 
						|
				osFilter === "all" ||
 | 
						|
				host.os_type?.toLowerCase() === osFilter.toLowerCase();
 | 
						|
 | 
						|
			// URL filter for hosts needing updates, inactive hosts, up-to-date hosts, stale hosts, or offline hosts
 | 
						|
			const filter = searchParams.get("filter");
 | 
						|
			const matchesUrlFilter =
 | 
						|
				(filter !== "needsUpdates" ||
 | 
						|
					(host.updatesCount && host.updatesCount > 0)) &&
 | 
						|
				(filter !== "inactive" ||
 | 
						|
					(host.effectiveStatus || host.status) === "inactive") &&
 | 
						|
				(filter !== "upToDate" || (!host.isStale && host.updatesCount === 0)) &&
 | 
						|
				(filter !== "stale" || host.isStale) &&
 | 
						|
				(filter !== "offline" || wsStatusMap[host.api_id]?.connected !== true);
 | 
						|
 | 
						|
			// Hide stale filter
 | 
						|
			const matchesHideStale = !hideStale || !host.isStale;
 | 
						|
 | 
						|
			return (
 | 
						|
				matchesSearch &&
 | 
						|
				matchesGroup &&
 | 
						|
				matchesStatus &&
 | 
						|
				matchesOs &&
 | 
						|
				matchesUrlFilter &&
 | 
						|
				matchesHideStale
 | 
						|
			);
 | 
						|
		});
 | 
						|
 | 
						|
		// Sorting
 | 
						|
		filtered.sort((a, b) => {
 | 
						|
			let aValue, bValue;
 | 
						|
 | 
						|
			switch (sortField) {
 | 
						|
				case "friendlyName":
 | 
						|
					aValue = a.friendly_name.toLowerCase();
 | 
						|
					bValue = b.friendly_name.toLowerCase();
 | 
						|
					break;
 | 
						|
				case "hostname":
 | 
						|
					aValue = a.hostname?.toLowerCase() || "zzz_no_hostname";
 | 
						|
					bValue = b.hostname?.toLowerCase() || "zzz_no_hostname";
 | 
						|
					break;
 | 
						|
				case "ip":
 | 
						|
					aValue = a.ip?.toLowerCase() || "zzz_no_ip";
 | 
						|
					bValue = b.ip?.toLowerCase() || "zzz_no_ip";
 | 
						|
					break;
 | 
						|
				case "group": {
 | 
						|
					// Handle multiple groups per host - use first group alphabetically for sorting
 | 
						|
					const aGroups = a.host_group_memberships || [];
 | 
						|
					const bGroups = b.host_group_memberships || [];
 | 
						|
					if (aGroups.length === 0) {
 | 
						|
						aValue = "zzz_ungrouped";
 | 
						|
					} else {
 | 
						|
						const aGroupNames = aGroups
 | 
						|
							.map((m) => m.host_groups?.name || "")
 | 
						|
							.filter((name) => name)
 | 
						|
							.sort();
 | 
						|
						aValue = aGroupNames[0] || "zzz_ungrouped";
 | 
						|
					}
 | 
						|
					if (bGroups.length === 0) {
 | 
						|
						bValue = "zzz_ungrouped";
 | 
						|
					} else {
 | 
						|
						const bGroupNames = bGroups
 | 
						|
							.map((m) => m.host_groups?.name || "")
 | 
						|
							.filter((name) => name)
 | 
						|
							.sort();
 | 
						|
						bValue = bGroupNames[0] || "zzz_ungrouped";
 | 
						|
					}
 | 
						|
					break;
 | 
						|
				}
 | 
						|
				case "os":
 | 
						|
					aValue = a.os_type?.toLowerCase() || "zzz_unknown";
 | 
						|
					bValue = b.os_type?.toLowerCase() || "zzz_unknown";
 | 
						|
					break;
 | 
						|
				case "os_version":
 | 
						|
					aValue = a.os_version?.toLowerCase() || "zzz_unknown";
 | 
						|
					bValue = b.os_version?.toLowerCase() || "zzz_unknown";
 | 
						|
					break;
 | 
						|
				case "agent_version":
 | 
						|
					aValue = a.agent_version?.toLowerCase() || "zzz_no_version";
 | 
						|
					bValue = b.agent_version?.toLowerCase() || "zzz_no_version";
 | 
						|
					break;
 | 
						|
				case "status":
 | 
						|
					aValue = a.effectiveStatus || a.status;
 | 
						|
					bValue = b.effectiveStatus || b.status;
 | 
						|
					break;
 | 
						|
				case "updates":
 | 
						|
					aValue = a.updatesCount || 0;
 | 
						|
					bValue = b.updatesCount || 0;
 | 
						|
					break;
 | 
						|
				case "last_update":
 | 
						|
					aValue = new Date(a.last_update);
 | 
						|
					bValue = new Date(b.last_update);
 | 
						|
					break;
 | 
						|
				case "notes":
 | 
						|
					aValue = (a.notes || "").toLowerCase();
 | 
						|
					bValue = (b.notes || "").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;
 | 
						|
		});
 | 
						|
 | 
						|
		return filtered;
 | 
						|
	}, [
 | 
						|
		hosts,
 | 
						|
		searchTerm,
 | 
						|
		groupFilter,
 | 
						|
		statusFilter,
 | 
						|
		osFilter,
 | 
						|
		sortField,
 | 
						|
		sortDirection,
 | 
						|
		searchParams,
 | 
						|
		hideStale,
 | 
						|
		wsStatusMap,
 | 
						|
	]);
 | 
						|
 | 
						|
	// Get unique OS types from hosts for dynamic dropdown
 | 
						|
	const uniqueOsTypes = useMemo(() => {
 | 
						|
		if (!hosts) return [];
 | 
						|
		const osTypes = new Set();
 | 
						|
		hosts.forEach((host) => {
 | 
						|
			if (host.os_type) {
 | 
						|
				osTypes.add(host.os_type);
 | 
						|
			}
 | 
						|
		});
 | 
						|
		return Array.from(osTypes).sort();
 | 
						|
	}, [hosts]);
 | 
						|
 | 
						|
	// Group hosts by selected field
 | 
						|
	const groupedHosts = useMemo(() => {
 | 
						|
		if (groupBy === "none") {
 | 
						|
			return { "All Hosts": filteredAndSortedHosts };
 | 
						|
		}
 | 
						|
 | 
						|
		const groups = {};
 | 
						|
		filteredAndSortedHosts.forEach((host) => {
 | 
						|
			if (groupBy === "group") {
 | 
						|
				// Handle multiple groups per host
 | 
						|
				const memberships = host.host_group_memberships || [];
 | 
						|
				if (memberships.length === 0) {
 | 
						|
					// Host has no groups, add to "Ungrouped"
 | 
						|
					if (!groups.Ungrouped) {
 | 
						|
						groups.Ungrouped = [];
 | 
						|
					}
 | 
						|
					groups.Ungrouped.push(host);
 | 
						|
				} else {
 | 
						|
					// Host has one or more groups, add to each group
 | 
						|
					memberships.forEach((membership) => {
 | 
						|
						const groupName = membership.host_groups?.name || "Unknown";
 | 
						|
						if (!groups[groupName]) {
 | 
						|
							groups[groupName] = [];
 | 
						|
						}
 | 
						|
						groups[groupName].push(host);
 | 
						|
					});
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				// Other grouping types (status, os, etc.)
 | 
						|
				let groupKey;
 | 
						|
				switch (groupBy) {
 | 
						|
					case "status":
 | 
						|
						groupKey =
 | 
						|
							(host.effectiveStatus || host.status).charAt(0).toUpperCase() +
 | 
						|
							(host.effectiveStatus || host.status).slice(1);
 | 
						|
						break;
 | 
						|
					case "os":
 | 
						|
						groupKey = host.os_type || "Unknown";
 | 
						|
						break;
 | 
						|
					default:
 | 
						|
						groupKey = "All Hosts";
 | 
						|
				}
 | 
						|
 | 
						|
				if (!groups[groupKey]) {
 | 
						|
					groups[groupKey] = [];
 | 
						|
				}
 | 
						|
				groups[groupKey].push(host);
 | 
						|
			}
 | 
						|
		});
 | 
						|
 | 
						|
		return groups;
 | 
						|
	}, [filteredAndSortedHosts, groupBy]);
 | 
						|
 | 
						|
	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" />
 | 
						|
		);
 | 
						|
	};
 | 
						|
 | 
						|
	// Column management functions
 | 
						|
	const updateColumnConfig = (newConfig) => {
 | 
						|
		setColumnConfig(newConfig);
 | 
						|
		localStorage.setItem("hosts-column-config", JSON.stringify(newConfig));
 | 
						|
	};
 | 
						|
 | 
						|
	const toggleColumnVisibility = (columnId) => {
 | 
						|
		const newConfig = columnConfig.map((col) =>
 | 
						|
			col.id === columnId ? { ...col, visible: !col.visible } : col,
 | 
						|
		);
 | 
						|
		updateColumnConfig(newConfig);
 | 
						|
	};
 | 
						|
 | 
						|
	const reorderColumns = (fromIndex, toIndex) => {
 | 
						|
		const newConfig = [...columnConfig];
 | 
						|
		const [movedColumn] = newConfig.splice(fromIndex, 1);
 | 
						|
		newConfig.splice(toIndex, 0, movedColumn);
 | 
						|
 | 
						|
		// Update order values
 | 
						|
		const updatedConfig = newConfig.map((col, index) => ({
 | 
						|
			...col,
 | 
						|
			order: index,
 | 
						|
		}));
 | 
						|
		updateColumnConfig(updatedConfig);
 | 
						|
	};
 | 
						|
 | 
						|
	const resetColumns = () => {
 | 
						|
		const defaultConfig = [
 | 
						|
			{ id: "select", label: "Select", visible: true, order: 0 },
 | 
						|
			{ id: "host", label: "Friendly Name", visible: true, order: 1 },
 | 
						|
			{ id: "hostname", label: "System Hostname", visible: true, order: 2 },
 | 
						|
			{ id: "ip", label: "IP Address", visible: false, order: 3 },
 | 
						|
			{ id: "group", label: "Group", visible: true, order: 4 },
 | 
						|
			{ id: "os", label: "OS", visible: true, order: 5 },
 | 
						|
			{ id: "os_version", label: "OS Version", visible: false, order: 6 },
 | 
						|
			{ id: "agent_version", label: "Agent Version", visible: true, order: 7 },
 | 
						|
			{
 | 
						|
				id: "auto_update",
 | 
						|
				label: "Agent Auto-Update",
 | 
						|
				visible: true,
 | 
						|
				order: 8,
 | 
						|
			},
 | 
						|
			{ id: "ws_status", label: "Connection", visible: true, order: 9 },
 | 
						|
			{ id: "status", label: "Status", visible: true, order: 10 },
 | 
						|
			{ id: "updates", label: "Updates", visible: true, order: 11 },
 | 
						|
			{ id: "notes", label: "Notes", visible: false, order: 12 },
 | 
						|
			{ id: "last_update", label: "Last Update", visible: true, order: 13 },
 | 
						|
			{ id: "actions", label: "Actions", visible: true, order: 14 },
 | 
						|
		];
 | 
						|
		updateColumnConfig(defaultConfig);
 | 
						|
	};
 | 
						|
 | 
						|
	// Get visible columns in order
 | 
						|
	const visibleColumns = columnConfig
 | 
						|
		.filter((col) => col.visible)
 | 
						|
		.sort((a, b) => a.order - b.order);
 | 
						|
 | 
						|
	// Helper function to render table cell content
 | 
						|
	const renderCellContent = (column, host) => {
 | 
						|
		switch (column.id) {
 | 
						|
			case "select":
 | 
						|
				return (
 | 
						|
					<button
 | 
						|
						type="button"
 | 
						|
						onClick={() => handleSelectHost(host.id)}
 | 
						|
						className="flex items-center gap-2 hover:text-secondary-700"
 | 
						|
					>
 | 
						|
						{selectedHosts.includes(host.id) ? (
 | 
						|
							<CheckSquare className="h-4 w-4 text-primary-600" />
 | 
						|
						) : (
 | 
						|
							<Square className="h-4 w-4 text-secondary-400" />
 | 
						|
						)}
 | 
						|
					</button>
 | 
						|
				);
 | 
						|
			case "host":
 | 
						|
				return (
 | 
						|
					<InlineEdit
 | 
						|
						value={host.friendly_name}
 | 
						|
						onSave={(newName) =>
 | 
						|
							updateFriendlyNameMutation.mutate({
 | 
						|
								hostId: host.id,
 | 
						|
								friendlyName: newName,
 | 
						|
							})
 | 
						|
						}
 | 
						|
						placeholder="Enter friendly name..."
 | 
						|
						maxLength={100}
 | 
						|
						linkTo={`/hosts/${host.id}`}
 | 
						|
						validate={(value) => {
 | 
						|
							if (!value.trim()) return "Friendly name is required";
 | 
						|
							if (value.trim().length < 1)
 | 
						|
								return "Friendly name must be at least 1 character";
 | 
						|
							if (value.trim().length > 100)
 | 
						|
								return "Friendly name must be less than 100 characters";
 | 
						|
							return null;
 | 
						|
						}}
 | 
						|
						className="w-full"
 | 
						|
					/>
 | 
						|
				);
 | 
						|
			case "hostname":
 | 
						|
				return (
 | 
						|
					<div className="text-sm text-secondary-900 dark:text-white font-mono">
 | 
						|
						{host.hostname || "N/A"}
 | 
						|
					</div>
 | 
						|
				);
 | 
						|
			case "ip":
 | 
						|
				return (
 | 
						|
					<div className="text-sm text-secondary-900 dark:text-white">
 | 
						|
						{host.ip || "N/A"}
 | 
						|
					</div>
 | 
						|
				);
 | 
						|
			case "group": {
 | 
						|
				// Extract group IDs from the new many-to-many structure
 | 
						|
				const groupIds =
 | 
						|
					host.host_group_memberships?.map(
 | 
						|
						(membership) => membership.host_groups.id,
 | 
						|
					) || [];
 | 
						|
				return (
 | 
						|
					<InlineMultiGroupEdit
 | 
						|
						key={`${host.id}-${groupIds.join(",")}`}
 | 
						|
						value={groupIds}
 | 
						|
						onSave={(newGroupIds) =>
 | 
						|
							updateHostGroupsMutation.mutate({
 | 
						|
								hostId: host.id,
 | 
						|
								groupIds: newGroupIds,
 | 
						|
							})
 | 
						|
						}
 | 
						|
						options={hostGroups || []}
 | 
						|
						placeholder="Select groups..."
 | 
						|
						className="w-full"
 | 
						|
					/>
 | 
						|
				);
 | 
						|
			}
 | 
						|
			case "os":
 | 
						|
				return (
 | 
						|
					<div className="flex items-center gap-2 text-sm text-secondary-900 dark:text-white">
 | 
						|
						<OSIcon osType={host.os_type} className="h-4 w-4" />
 | 
						|
						<span>{getOSDisplayName(host.os_type)}</span>
 | 
						|
					</div>
 | 
						|
				);
 | 
						|
			case "os_version":
 | 
						|
				return (
 | 
						|
					<div className="text-sm text-secondary-900 dark:text-white">
 | 
						|
						{host.os_version || "N/A"}
 | 
						|
					</div>
 | 
						|
				);
 | 
						|
			case "agent_version":
 | 
						|
				return (
 | 
						|
					<div className="text-sm text-secondary-900 dark:text-white">
 | 
						|
						{host.agent_version || "N/A"}
 | 
						|
					</div>
 | 
						|
				);
 | 
						|
			case "auto_update":
 | 
						|
				return (
 | 
						|
					<InlineToggle
 | 
						|
						value={host.auto_update}
 | 
						|
						onSave={(autoUpdate) =>
 | 
						|
							toggleAutoUpdateMutation.mutate({
 | 
						|
								hostId: host.id,
 | 
						|
								autoUpdate: autoUpdate,
 | 
						|
							})
 | 
						|
						}
 | 
						|
						trueLabel="Yes"
 | 
						|
						falseLabel="No"
 | 
						|
					/>
 | 
						|
				);
 | 
						|
			case "ws_status": {
 | 
						|
				const wsStatus = wsStatusMap[host.api_id];
 | 
						|
				if (!wsStatus) {
 | 
						|
					return (
 | 
						|
						<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
 | 
						|
							<div className="w-2 h-2 bg-gray-400 rounded-full mr-1.5"></div>
 | 
						|
							Unknown
 | 
						|
						</span>
 | 
						|
					);
 | 
						|
				}
 | 
						|
				return (
 | 
						|
					<span
 | 
						|
						className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
 | 
						|
							wsStatus.connected
 | 
						|
								? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
 | 
						|
								: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
 | 
						|
						}`}
 | 
						|
						title={
 | 
						|
							wsStatus.connected
 | 
						|
								? `Agent connected via ${wsStatus.secure ? "WSS (secure)" : "WS (insecure)"}`
 | 
						|
								: "Agent not connected"
 | 
						|
						}
 | 
						|
					>
 | 
						|
						<div
 | 
						|
							className={`w-2 h-2 rounded-full mr-1.5 ${
 | 
						|
								wsStatus.connected ? "bg-green-500 animate-pulse" : "bg-red-500"
 | 
						|
							}`}
 | 
						|
						></div>
 | 
						|
						{wsStatus.connected ? (wsStatus.secure ? "WSS" : "WS") : "Offline"}
 | 
						|
					</span>
 | 
						|
				);
 | 
						|
			}
 | 
						|
			case "status":
 | 
						|
				return (
 | 
						|
					<div className="text-sm text-secondary-900 dark:text-white">
 | 
						|
						{(host.effectiveStatus || host.status).charAt(0).toUpperCase() +
 | 
						|
							(host.effectiveStatus || host.status).slice(1)}
 | 
						|
					</div>
 | 
						|
				);
 | 
						|
			case "updates":
 | 
						|
				return (
 | 
						|
					<button
 | 
						|
						type="button"
 | 
						|
						onClick={() =>
 | 
						|
							navigate(`/packages?host=${host.id}&filter=outdated`)
 | 
						|
						}
 | 
						|
						className="text-sm text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 font-medium hover:underline"
 | 
						|
						title="View outdated packages for this host"
 | 
						|
					>
 | 
						|
						{host.updatesCount || 0}
 | 
						|
					</button>
 | 
						|
				);
 | 
						|
			case "last_update":
 | 
						|
				return (
 | 
						|
					<div className="text-sm text-secondary-500 dark:text-secondary-300">
 | 
						|
						{formatRelativeTime(host.last_update)}
 | 
						|
					</div>
 | 
						|
				);
 | 
						|
			case "notes":
 | 
						|
				return (
 | 
						|
					<div className="text-sm text-secondary-900 dark:text-white max-w-xs">
 | 
						|
						{host.notes ? (
 | 
						|
							<div className="truncate" title={host.notes}>
 | 
						|
								{host.notes}
 | 
						|
							</div>
 | 
						|
						) : (
 | 
						|
							<span className="text-secondary-400 dark:text-secondary-500 italic">
 | 
						|
								No notes
 | 
						|
							</span>
 | 
						|
						)}
 | 
						|
					</div>
 | 
						|
				);
 | 
						|
			case "actions":
 | 
						|
				return (
 | 
						|
					<Link
 | 
						|
						to={`/hosts/${host.id}`}
 | 
						|
						className="text-primary-600 hover:text-primary-900 flex items-center gap-1"
 | 
						|
					>
 | 
						|
						View
 | 
						|
						<ExternalLink className="h-3 w-3" />
 | 
						|
					</Link>
 | 
						|
				);
 | 
						|
			default:
 | 
						|
				return null;
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	const handleHostCreated = (newHost) => {
 | 
						|
		queryClient.invalidateQueries(["hosts"]);
 | 
						|
		// Navigate to host detail page to show credentials and setup instructions
 | 
						|
		navigate(`/hosts/${newHost.hostId}`);
 | 
						|
	};
 | 
						|
 | 
						|
	// Stats card click handlers
 | 
						|
	const handleTotalHostsClick = () => {
 | 
						|
		// Clear all filters to show all hosts
 | 
						|
		setSearchTerm("");
 | 
						|
		setGroupFilter("all");
 | 
						|
		setStatusFilter("all");
 | 
						|
		setOsFilter("all");
 | 
						|
		setGroupBy("none");
 | 
						|
		setHideStale(false);
 | 
						|
		setShowFilters(false);
 | 
						|
		// Clear URL parameters to ensure no filters are applied
 | 
						|
		navigate("/hosts", { replace: true });
 | 
						|
	};
 | 
						|
 | 
						|
	const handleUpToDateClick = () => {
 | 
						|
		// Filter to show only up-to-date hosts
 | 
						|
		setStatusFilter("active");
 | 
						|
		setShowFilters(true);
 | 
						|
		// Use the upToDate URL filter
 | 
						|
		const newSearchParams = new URLSearchParams(window.location.search);
 | 
						|
		newSearchParams.set("filter", "upToDate");
 | 
						|
		navigate(`/hosts?${newSearchParams.toString()}`, { replace: true });
 | 
						|
	};
 | 
						|
 | 
						|
	const handleNeedsUpdatesClick = () => {
 | 
						|
		// Filter to show hosts needing updates (regardless of status)
 | 
						|
		setStatusFilter("all");
 | 
						|
		setShowFilters(true);
 | 
						|
		// We'll use the existing needsUpdates URL filter logic
 | 
						|
		const newSearchParams = new URLSearchParams(window.location.search);
 | 
						|
		newSearchParams.set("filter", "needsUpdates");
 | 
						|
		navigate(`/hosts?${newSearchParams.toString()}`, { replace: true });
 | 
						|
	};
 | 
						|
 | 
						|
	const handleConnectionStatusClick = () => {
 | 
						|
		// Filter to show offline hosts (not connected via WebSocket)
 | 
						|
		setStatusFilter("all");
 | 
						|
		setShowFilters(true);
 | 
						|
		// Use a new URL filter for connection status
 | 
						|
		const newSearchParams = new URLSearchParams(window.location.search);
 | 
						|
		newSearchParams.set("filter", "offline");
 | 
						|
		navigate(`/hosts?${newSearchParams.toString()}`, { replace: true });
 | 
						|
	};
 | 
						|
 | 
						|
	if (isLoading) {
 | 
						|
		return (
 | 
						|
			<div className="flex items-center justify-center h-64">
 | 
						|
				<RefreshCw className="h-8 w-8 animate-spin text-primary-600" />
 | 
						|
			</div>
 | 
						|
		);
 | 
						|
	}
 | 
						|
 | 
						|
	if (error) {
 | 
						|
		return (
 | 
						|
			<div className="bg-danger-50 border border-danger-200 rounded-md p-4">
 | 
						|
				<div className="flex">
 | 
						|
					<AlertTriangle className="h-5 w-5 text-danger-400" />
 | 
						|
					<div className="ml-3">
 | 
						|
						<h3 className="text-sm font-medium text-danger-800">
 | 
						|
							Error loading hosts
 | 
						|
						</h3>
 | 
						|
						<p className="text-sm text-danger-700 mt-1">
 | 
						|
							{error.message || "Failed to load hosts"}
 | 
						|
						</p>
 | 
						|
						<button
 | 
						|
							type="button"
 | 
						|
							onClick={() => refetch()}
 | 
						|
							className="mt-2 btn-danger text-xs"
 | 
						|
						>
 | 
						|
							Try again
 | 
						|
						</button>
 | 
						|
					</div>
 | 
						|
				</div>
 | 
						|
			</div>
 | 
						|
		);
 | 
						|
	}
 | 
						|
 | 
						|
	return (
 | 
						|
		<div className="h-[calc(100vh-7rem)] flex flex-col overflow-hidden">
 | 
						|
			{/* Page Header */}
 | 
						|
			<div className="flex items-center justify-between mb-6">
 | 
						|
				<div>
 | 
						|
					<h1 className="text-2xl font-semibold text-secondary-900 dark:text-white">
 | 
						|
						Hosts
 | 
						|
					</h1>
 | 
						|
					<p className="text-sm text-secondary-600 dark:text-secondary-400 mt-1">
 | 
						|
						Manage and monitor your connected hosts
 | 
						|
					</p>
 | 
						|
				</div>
 | 
						|
				<div className="flex items-center gap-3">
 | 
						|
					<button
 | 
						|
						type="button"
 | 
						|
						onClick={() => refetch()}
 | 
						|
						disabled={isFetching}
 | 
						|
						className="btn-outline flex items-center justify-center p-2"
 | 
						|
						title="Refresh hosts data"
 | 
						|
					>
 | 
						|
						<RefreshCw
 | 
						|
							className={`h-4 w-4 ${isFetching ? "animate-spin" : ""}`}
 | 
						|
						/>
 | 
						|
					</button>
 | 
						|
					<button
 | 
						|
						type="button"
 | 
						|
						onClick={() => setShowAddModal(true)}
 | 
						|
						className="btn-primary flex items-center gap-2"
 | 
						|
					>
 | 
						|
						<Plus className="h-4 w-4" />
 | 
						|
						Add Host
 | 
						|
					</button>
 | 
						|
				</div>
 | 
						|
			</div>
 | 
						|
 | 
						|
			{/* Stats Summary */}
 | 
						|
			<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mb-6">
 | 
						|
				<button
 | 
						|
					type="button"
 | 
						|
					className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
 | 
						|
					onClick={handleTotalHostsClick}
 | 
						|
				>
 | 
						|
					<div className="flex items-center">
 | 
						|
						<Server className="h-5 w-5 text-primary-600 mr-2" />
 | 
						|
						<div>
 | 
						|
							<p className="text-sm text-secondary-500 dark:text-white">
 | 
						|
								Total Hosts
 | 
						|
							</p>
 | 
						|
							<p className="text-xl font-semibold text-secondary-900 dark:text-white">
 | 
						|
								{hosts?.length || 0}
 | 
						|
							</p>
 | 
						|
						</div>
 | 
						|
					</div>
 | 
						|
				</button>
 | 
						|
				<button
 | 
						|
					type="button"
 | 
						|
					className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
 | 
						|
					onClick={handleUpToDateClick}
 | 
						|
				>
 | 
						|
					<div className="flex items-center">
 | 
						|
						<CheckCircle className="h-5 w-5 text-success-600 mr-2" />
 | 
						|
						<div>
 | 
						|
							<p className="text-sm text-secondary-500 dark:text-white">
 | 
						|
								Up to Date
 | 
						|
							</p>
 | 
						|
							<p className="text-xl font-semibold text-secondary-900 dark:text-white">
 | 
						|
								{hosts?.filter((h) => !h.isStale && h.updatesCount === 0)
 | 
						|
									.length || 0}
 | 
						|
							</p>
 | 
						|
						</div>
 | 
						|
					</div>
 | 
						|
				</button>
 | 
						|
				<button
 | 
						|
					type="button"
 | 
						|
					className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
 | 
						|
					onClick={handleNeedsUpdatesClick}
 | 
						|
				>
 | 
						|
					<div className="flex items-center">
 | 
						|
						<Clock className="h-5 w-5 text-warning-600 mr-2" />
 | 
						|
						<div>
 | 
						|
							<p className="text-sm text-secondary-500 dark:text-white">
 | 
						|
								Needs Updates
 | 
						|
							</p>
 | 
						|
							<p className="text-xl font-semibold text-secondary-900 dark:text-white">
 | 
						|
								{hosts?.filter((h) => h.updatesCount > 0).length || 0}
 | 
						|
							</p>
 | 
						|
						</div>
 | 
						|
					</div>
 | 
						|
				</button>
 | 
						|
				<button
 | 
						|
					type="button"
 | 
						|
					className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
 | 
						|
					onClick={handleConnectionStatusClick}
 | 
						|
				>
 | 
						|
					<div className="flex items-center">
 | 
						|
						<Wifi className="h-5 w-5 text-primary-600 mr-2" />
 | 
						|
						<div className="flex-1">
 | 
						|
							<p className="text-sm text-secondary-500 dark:text-white mb-1">
 | 
						|
								Connection Status
 | 
						|
							</p>
 | 
						|
							{(() => {
 | 
						|
								const connectedCount =
 | 
						|
									hosts?.filter(
 | 
						|
										(h) => wsStatusMap[h.api_id]?.connected === true,
 | 
						|
									).length || 0;
 | 
						|
								const offlineCount =
 | 
						|
									hosts?.filter(
 | 
						|
										(h) => wsStatusMap[h.api_id]?.connected !== true,
 | 
						|
									).length || 0;
 | 
						|
								return (
 | 
						|
									<div className="flex gap-4">
 | 
						|
										<div className="flex items-center gap-1">
 | 
						|
											<div className="w-2 h-2 bg-green-500 rounded-full"></div>
 | 
						|
											<span className="text-sm font-medium text-secondary-900 dark:text-white">
 | 
						|
												{connectedCount}
 | 
						|
											</span>
 | 
						|
											<span className="text-xs text-secondary-500 dark:text-secondary-400">
 | 
						|
												Connected
 | 
						|
											</span>
 | 
						|
										</div>
 | 
						|
										<div className="flex items-center gap-1">
 | 
						|
											<div className="w-2 h-2 bg-red-500 rounded-full"></div>
 | 
						|
											<span className="text-sm font-medium text-secondary-900 dark:text-white">
 | 
						|
												{offlineCount}
 | 
						|
											</span>
 | 
						|
											<span className="text-xs text-secondary-500 dark:text-secondary-400">
 | 
						|
												Offline
 | 
						|
											</span>
 | 
						|
										</div>
 | 
						|
									</div>
 | 
						|
								);
 | 
						|
							})()}
 | 
						|
						</div>
 | 
						|
					</div>
 | 
						|
				</button>
 | 
						|
			</div>
 | 
						|
 | 
						|
			{/* Hosts List */}
 | 
						|
			<div className="card flex-1 flex flex-col overflow-hidden min-h-0">
 | 
						|
				<div className="px-4 py-4 sm:p-4 flex-1 flex flex-col overflow-hidden min-h-0">
 | 
						|
					<div className="flex items-center justify-end mb-4">
 | 
						|
						{selectedHosts.length > 0 && (
 | 
						|
							<div className="flex items-center gap-3">
 | 
						|
								<span className="text-sm text-secondary-600">
 | 
						|
									{selectedHosts.length} host
 | 
						|
									{selectedHosts.length !== 1 ? "s" : ""} selected
 | 
						|
								</span>
 | 
						|
								<button
 | 
						|
									type="button"
 | 
						|
									onClick={() => setShowBulkAssignModal(true)}
 | 
						|
									className="btn-outline flex items-center gap-2"
 | 
						|
								>
 | 
						|
									<Users className="h-4 w-4" />
 | 
						|
									Assign to Group
 | 
						|
								</button>
 | 
						|
								<button
 | 
						|
									type="button"
 | 
						|
									onClick={() => setShowBulkDeleteModal(true)}
 | 
						|
									className="btn-danger flex items-center gap-2"
 | 
						|
								>
 | 
						|
									<Trash2 className="h-4 w-4" />
 | 
						|
									Delete
 | 
						|
								</button>
 | 
						|
								<button
 | 
						|
									type="button"
 | 
						|
									onClick={() => setSelectedHosts([])}
 | 
						|
									className="text-sm text-secondary-500 hover:text-secondary-700"
 | 
						|
								>
 | 
						|
									Clear Selection
 | 
						|
								</button>
 | 
						|
							</div>
 | 
						|
						)}
 | 
						|
					</div>
 | 
						|
 | 
						|
					{/* Table Controls */}
 | 
						|
					<div className="mb-4 space-y-4">
 | 
						|
						{/* Search and Filter Bar */}
 | 
						|
						<div className="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 h-4 w-4 text-secondary-400 dark:text-secondary-500" />
 | 
						|
									<input
 | 
						|
										type="text"
 | 
						|
										placeholder="Search hosts, IP addresses, or OS..."
 | 
						|
										value={searchTerm}
 | 
						|
										onChange={(e) => setSearchTerm(e.target.value)}
 | 
						|
										className="pl-10 pr-4 py-2 w-full border border-secondary-300 dark:border-secondary-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white placeholder-secondary-500 dark:placeholder-secondary-400"
 | 
						|
									/>
 | 
						|
								</div>
 | 
						|
							</div>
 | 
						|
							<div className="flex gap-2">
 | 
						|
								<button
 | 
						|
									type="button"
 | 
						|
									onClick={() => setShowFilters(!showFilters)}
 | 
						|
									className={`btn-outline flex items-center gap-2 ${showFilters ? "bg-primary-50 border-primary-300" : ""}`}
 | 
						|
								>
 | 
						|
									<Filter className="h-4 w-4" />
 | 
						|
									Filters
 | 
						|
								</button>
 | 
						|
								<button
 | 
						|
									type="button"
 | 
						|
									onClick={() => setShowColumnSettings(true)}
 | 
						|
									className="btn-outline flex items-center gap-2"
 | 
						|
								>
 | 
						|
									<Columns className="h-4 w-4" />
 | 
						|
									Columns
 | 
						|
								</button>
 | 
						|
								<div className="relative">
 | 
						|
									<select
 | 
						|
										value={groupBy}
 | 
						|
										onChange={(e) => setGroupBy(e.target.value)}
 | 
						|
										className="appearance-none bg-white dark:bg-secondary-800 border-2 border-secondary-300 dark:border-secondary-600 rounded-lg px-2 py-2 pr-6 text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500 text-secondary-900 dark:text-white hover:border-secondary-400 dark:hover:border-secondary-500 transition-colors min-w-[120px]"
 | 
						|
									>
 | 
						|
										<option value="none">No Grouping</option>
 | 
						|
										<option value="group">By Group</option>
 | 
						|
										<option value="status">By Status</option>
 | 
						|
										<option value="os">By OS</option>
 | 
						|
									</select>
 | 
						|
									<ChevronDown className="absolute right-1 top-1/2 transform -translate-y-1/2 h-4 w-4 text-secondary-400 dark:text-secondary-500 pointer-events-none" />
 | 
						|
								</div>
 | 
						|
								<button
 | 
						|
									type="button"
 | 
						|
									onClick={() => setHideStale(!hideStale)}
 | 
						|
									className={`btn-outline flex items-center gap-2 ${hideStale ? "bg-primary-50 border-primary-300" : ""}`}
 | 
						|
								>
 | 
						|
									<AlertTriangle className="h-4 w-4" />
 | 
						|
									Hide Stale
 | 
						|
								</button>
 | 
						|
							</div>
 | 
						|
						</div>
 | 
						|
 | 
						|
						{/* Advanced Filters */}
 | 
						|
						{showFilters && (
 | 
						|
							<div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg border dark:border-secondary-600">
 | 
						|
								<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
 | 
						|
									<div>
 | 
						|
										<label
 | 
						|
											htmlFor={hostGroupFilterId}
 | 
						|
											className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
										>
 | 
						|
											Host Group
 | 
						|
										</label>
 | 
						|
										<select
 | 
						|
											id={hostGroupFilterId}
 | 
						|
											value={groupFilter}
 | 
						|
											onChange={(e) => setGroupFilter(e.target.value)}
 | 
						|
											className="w-full border border-secondary-300 dark:border-secondary-600 rounded-lg px-3 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white"
 | 
						|
										>
 | 
						|
											<option value="all">All Groups</option>
 | 
						|
											<option value="ungrouped">Ungrouped</option>
 | 
						|
											{hostGroups?.map((group) => (
 | 
						|
												<option key={group.id} value={group.id}>
 | 
						|
													{group.name}
 | 
						|
												</option>
 | 
						|
											))}
 | 
						|
										</select>
 | 
						|
									</div>
 | 
						|
									<div>
 | 
						|
										<label
 | 
						|
											htmlFor={statusFilterId}
 | 
						|
											className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
										>
 | 
						|
											Status
 | 
						|
										</label>
 | 
						|
										<select
 | 
						|
											id={statusFilterId}
 | 
						|
											value={statusFilter}
 | 
						|
											onChange={(e) => setStatusFilter(e.target.value)}
 | 
						|
											className="w-full border border-secondary-300 dark:border-secondary-600 rounded-lg px-3 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white"
 | 
						|
										>
 | 
						|
											<option value="all">All Status</option>
 | 
						|
											<option value="active">Active</option>
 | 
						|
											<option value="pending">Pending</option>
 | 
						|
											<option value="inactive">Inactive</option>
 | 
						|
											<option value="error">Error</option>
 | 
						|
										</select>
 | 
						|
									</div>
 | 
						|
									<div>
 | 
						|
										<label
 | 
						|
											htmlFor={osFilterId}
 | 
						|
											className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
										>
 | 
						|
											Operating System
 | 
						|
										</label>
 | 
						|
										<select
 | 
						|
											id={osFilterId}
 | 
						|
											value={osFilter}
 | 
						|
											onChange={(e) => setOsFilter(e.target.value)}
 | 
						|
											className="w-full border border-secondary-300 dark:border-secondary-600 rounded-lg px-3 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white"
 | 
						|
										>
 | 
						|
											<option value="all">All OS</option>
 | 
						|
											{uniqueOsTypes.map((osType) => (
 | 
						|
												<option key={osType} value={osType.toLowerCase()}>
 | 
						|
													{osType}
 | 
						|
												</option>
 | 
						|
											))}
 | 
						|
										</select>
 | 
						|
									</div>
 | 
						|
									<div className="flex items-end">
 | 
						|
										<button
 | 
						|
											type="button"
 | 
						|
											onClick={() => {
 | 
						|
												setSearchTerm("");
 | 
						|
												setGroupFilter("all");
 | 
						|
												setStatusFilter("all");
 | 
						|
												setOsFilter("all");
 | 
						|
												setGroupBy("none");
 | 
						|
												setHideStale(false);
 | 
						|
											}}
 | 
						|
											className="btn-outline w-full"
 | 
						|
										>
 | 
						|
											Clear Filters
 | 
						|
										</button>
 | 
						|
									</div>
 | 
						|
								</div>
 | 
						|
							</div>
 | 
						|
						)}
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div className="flex-1 overflow-hidden">
 | 
						|
						{!hosts || hosts.length === 0 ? (
 | 
						|
							<div className="text-center py-8">
 | 
						|
								<Server className="h-12 w-12 text-secondary-400 mx-auto mb-4" />
 | 
						|
								<p className="text-secondary-500">No hosts registered yet</p>
 | 
						|
								<p className="text-sm text-secondary-400 mt-2">
 | 
						|
									Click "Add Host" to manually register a new host and get API
 | 
						|
									credentials
 | 
						|
								</p>
 | 
						|
							</div>
 | 
						|
						) : filteredAndSortedHosts.length === 0 ? (
 | 
						|
							<div className="text-center py-8">
 | 
						|
								<Search className="h-12 w-12 text-secondary-400 mx-auto mb-4" />
 | 
						|
								<p className="text-secondary-500">
 | 
						|
									No hosts match your current filters
 | 
						|
								</p>
 | 
						|
								<p className="text-sm text-secondary-400 mt-2">
 | 
						|
									Try adjusting your search terms or filters to see more results
 | 
						|
								</p>
 | 
						|
							</div>
 | 
						|
						) : (
 | 
						|
							<div className="h-full overflow-auto">
 | 
						|
								<div className="space-y-6">
 | 
						|
									{Object.entries(groupedHosts).map(
 | 
						|
										([groupName, groupHosts]) => (
 | 
						|
											<div key={groupName} className="space-y-3">
 | 
						|
												{/* Group Header */}
 | 
						|
												{groupBy !== "none" && (
 | 
						|
													<div className="flex items-center justify-between bg-secondary-100 dark:bg-secondary-700 px-4 py-2 rounded-lg">
 | 
						|
														<h3 className="text-sm font-medium text-secondary-900 dark:text-white">
 | 
						|
															{groupName} ({groupHosts.length})
 | 
						|
														</h3>
 | 
						|
													</div>
 | 
						|
												)}
 | 
						|
 | 
						|
												{/* Table for this group */}
 | 
						|
												<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>
 | 
						|
																{visibleColumns.map((column) => (
 | 
						|
																	<th
 | 
						|
																		key={column.id}
 | 
						|
																		className="px-4 py-2 text-center text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider"
 | 
						|
																	>
 | 
						|
																		{column.id === "select" ? (
 | 
						|
																			<button
 | 
						|
																				type="button"
 | 
						|
																				onClick={handleSelectAll}
 | 
						|
																				className="flex items-center gap-2 hover:text-secondary-700"
 | 
						|
																			>
 | 
						|
																				{selectedHosts.length ===
 | 
						|
																				groupHosts.length ? (
 | 
						|
																					<CheckSquare className="h-4 w-4" />
 | 
						|
																				) : (
 | 
						|
																					<Square className="h-4 w-4" />
 | 
						|
																				)}
 | 
						|
																			</button>
 | 
						|
																		) : column.id === "host" ? (
 | 
						|
																			<button
 | 
						|
																				type="button"
 | 
						|
																				onClick={() =>
 | 
						|
																					handleSort("friendlyName")
 | 
						|
																				}
 | 
						|
																				className="flex items-center gap-2 hover:text-secondary-700"
 | 
						|
																			>
 | 
						|
																				{column.label}
 | 
						|
																				{getSortIcon("friendlyName")}
 | 
						|
																			</button>
 | 
						|
																		) : column.id === "hostname" ? (
 | 
						|
																			<button
 | 
						|
																				type="button"
 | 
						|
																				onClick={() => handleSort("hostname")}
 | 
						|
																				className="flex items-center gap-2 hover:text-secondary-700"
 | 
						|
																			>
 | 
						|
																				{column.label}
 | 
						|
																				{getSortIcon("hostname")}
 | 
						|
																			</button>
 | 
						|
																		) : column.id === "ip" ? (
 | 
						|
																			<button
 | 
						|
																				type="button"
 | 
						|
																				onClick={() => handleSort("ip")}
 | 
						|
																				className="flex items-center gap-2 hover:text-secondary-700"
 | 
						|
																			>
 | 
						|
																				{column.label}
 | 
						|
																				{getSortIcon("ip")}
 | 
						|
																			</button>
 | 
						|
																		) : column.id === "group" ? (
 | 
						|
																			<button
 | 
						|
																				type="button"
 | 
						|
																				onClick={() => handleSort("group")}
 | 
						|
																				className="flex items-center gap-2 hover:text-secondary-700"
 | 
						|
																			>
 | 
						|
																				{column.label}
 | 
						|
																				{getSortIcon("group")}
 | 
						|
																			</button>
 | 
						|
																		) : column.id === "os" ? (
 | 
						|
																			<button
 | 
						|
																				type="button"
 | 
						|
																				onClick={() => handleSort("os")}
 | 
						|
																				className="flex items-center gap-2 hover:text-secondary-700"
 | 
						|
																			>
 | 
						|
																				{column.label}
 | 
						|
																				{getSortIcon("os")}
 | 
						|
																			</button>
 | 
						|
																		) : column.id === "os_version" ? (
 | 
						|
																			<button
 | 
						|
																				type="button"
 | 
						|
																				onClick={() => handleSort("os_version")}
 | 
						|
																				className="flex items-center gap-2 hover:text-secondary-700"
 | 
						|
																			>
 | 
						|
																				{column.label}
 | 
						|
																				{getSortIcon("os_version")}
 | 
						|
																			</button>
 | 
						|
																		) : column.id === "agent_version" ? (
 | 
						|
																			<button
 | 
						|
																				type="button"
 | 
						|
																				onClick={() =>
 | 
						|
																					handleSort("agent_version")
 | 
						|
																				}
 | 
						|
																				className="flex items-center gap-2 hover:text-secondary-700"
 | 
						|
																			>
 | 
						|
																				{column.label}
 | 
						|
																				{getSortIcon("agent_version")}
 | 
						|
																			</button>
 | 
						|
																		) : column.id === "auto_update" ? (
 | 
						|
																			<div className="flex items-center gap-2 font-normal text-xs text-secondary-500 dark:text-secondary-300 normal-case tracking-wider">
 | 
						|
																				{column.label}
 | 
						|
																			</div>
 | 
						|
																		) : column.id === "ws_status" ? (
 | 
						|
																			<div className="flex items-center gap-2 font-normal text-xs text-secondary-500 dark:text-secondary-300 normal-case tracking-wider">
 | 
						|
																				<Wifi className="h-3 w-3" />
 | 
						|
																				{column.label}
 | 
						|
																			</div>
 | 
						|
																		) : column.id === "status" ? (
 | 
						|
																			<button
 | 
						|
																				type="button"
 | 
						|
																				onClick={() => handleSort("status")}
 | 
						|
																				className="flex items-center gap-2 hover:text-secondary-700"
 | 
						|
																			>
 | 
						|
																				{column.label}
 | 
						|
																				{getSortIcon("status")}
 | 
						|
																			</button>
 | 
						|
																		) : column.id === "updates" ? (
 | 
						|
																			<button
 | 
						|
																				type="button"
 | 
						|
																				onClick={() => handleSort("updates")}
 | 
						|
																				className="flex items-center gap-2 hover:text-secondary-700"
 | 
						|
																			>
 | 
						|
																				{column.label}
 | 
						|
																				{getSortIcon("updates")}
 | 
						|
																			</button>
 | 
						|
																		) : column.id === "last_update" ? (
 | 
						|
																			<button
 | 
						|
																				type="button"
 | 
						|
																				onClick={() =>
 | 
						|
																					handleSort("last_update")
 | 
						|
																				}
 | 
						|
																				className="flex items-center gap-2 hover:text-secondary-700"
 | 
						|
																			>
 | 
						|
																				{column.label}
 | 
						|
																				{getSortIcon("last_update")}
 | 
						|
																			</button>
 | 
						|
																		) : (
 | 
						|
																			column.label
 | 
						|
																		)}
 | 
						|
																	</th>
 | 
						|
																))}
 | 
						|
															</tr>
 | 
						|
														</thead>
 | 
						|
														<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600">
 | 
						|
															{groupHosts.map((host) => {
 | 
						|
																const isInactive =
 | 
						|
																	(host.effectiveStatus || host.status) ===
 | 
						|
																	"inactive";
 | 
						|
																const isSelected = selectedHosts.includes(
 | 
						|
																	host.id,
 | 
						|
																);
 | 
						|
 | 
						|
																let rowClasses =
 | 
						|
																	"hover:bg-secondary-50 dark:hover:bg-secondary-700";
 | 
						|
 | 
						|
																if (isSelected) {
 | 
						|
																	rowClasses +=
 | 
						|
																		" bg-primary-50 dark:bg-primary-600";
 | 
						|
																} else if (isInactive) {
 | 
						|
																	rowClasses += " bg-red-50 dark:bg-red-900/20";
 | 
						|
																}
 | 
						|
 | 
						|
																return (
 | 
						|
																	<tr key={host.id} className={rowClasses}>
 | 
						|
																		{visibleColumns.map((column) => (
 | 
						|
																			<td
 | 
						|
																				key={column.id}
 | 
						|
																				className="px-4 py-2 whitespace-nowrap text-center"
 | 
						|
																			>
 | 
						|
																				{renderCellContent(column, host)}
 | 
						|
																			</td>
 | 
						|
																		))}
 | 
						|
																	</tr>
 | 
						|
																);
 | 
						|
															})}
 | 
						|
														</tbody>
 | 
						|
													</table>
 | 
						|
												</div>
 | 
						|
											</div>
 | 
						|
										),
 | 
						|
									)}
 | 
						|
								</div>
 | 
						|
							</div>
 | 
						|
						)}
 | 
						|
					</div>
 | 
						|
				</div>
 | 
						|
			</div>
 | 
						|
 | 
						|
			{/* Modals */}
 | 
						|
			<AddHostModal
 | 
						|
				isOpen={showAddModal}
 | 
						|
				onClose={() => setShowAddModal(false)}
 | 
						|
				onSuccess={handleHostCreated}
 | 
						|
			/>
 | 
						|
 | 
						|
			{/* Bulk Assign Modal */}
 | 
						|
			{showBulkAssignModal && (
 | 
						|
				<BulkAssignModal
 | 
						|
					selectedHosts={selectedHosts}
 | 
						|
					hosts={hosts}
 | 
						|
					onClose={() => setShowBulkAssignModal(false)}
 | 
						|
					onAssign={handleBulkAssign}
 | 
						|
					isLoading={bulkUpdateGroupMutation.isPending}
 | 
						|
				/>
 | 
						|
			)}
 | 
						|
 | 
						|
			{/* Bulk Delete Modal */}
 | 
						|
			{showBulkDeleteModal && (
 | 
						|
				<BulkDeleteModal
 | 
						|
					selectedHosts={selectedHosts}
 | 
						|
					hosts={hosts}
 | 
						|
					onClose={() => setShowBulkDeleteModal(false)}
 | 
						|
					onDelete={handleBulkDelete}
 | 
						|
					isLoading={bulkDeleteMutation.isPending}
 | 
						|
				/>
 | 
						|
			)}
 | 
						|
 | 
						|
			{/* Column Settings Modal */}
 | 
						|
			{showColumnSettings && (
 | 
						|
				<ColumnSettingsModal
 | 
						|
					columnConfig={columnConfig}
 | 
						|
					onClose={() => setShowColumnSettings(false)}
 | 
						|
					onToggleVisibility={toggleColumnVisibility}
 | 
						|
					onReorder={reorderColumns}
 | 
						|
					onReset={resetColumns}
 | 
						|
				/>
 | 
						|
			)}
 | 
						|
		</div>
 | 
						|
	);
 | 
						|
};
 | 
						|
 | 
						|
// Bulk Assign Modal Component
 | 
						|
const BulkAssignModal = ({
 | 
						|
	selectedHosts,
 | 
						|
	hosts,
 | 
						|
	onClose,
 | 
						|
	onAssign,
 | 
						|
	isLoading,
 | 
						|
}) => {
 | 
						|
	const [selectedGroupIds, setSelectedGroupIds] = useState([]);
 | 
						|
 | 
						|
	// Fetch host groups for selection
 | 
						|
	const { data: hostGroups } = useQuery({
 | 
						|
		queryKey: ["hostGroups"],
 | 
						|
		queryFn: () => hostGroupsAPI.list().then((res) => res.data),
 | 
						|
	});
 | 
						|
 | 
						|
	const selectedHostNames = hosts
 | 
						|
		.filter((host) => selectedHosts.includes(host.id))
 | 
						|
		.map((host) => host.friendly_name);
 | 
						|
 | 
						|
	const handleSubmit = (e) => {
 | 
						|
		e.preventDefault();
 | 
						|
		onAssign(selectedGroupIds);
 | 
						|
	};
 | 
						|
 | 
						|
	const toggleGroup = (groupId) => {
 | 
						|
		setSelectedGroupIds((prev) => {
 | 
						|
			if (prev.includes(groupId)) {
 | 
						|
				return prev.filter((id) => id !== groupId);
 | 
						|
			} else {
 | 
						|
				return [...prev, groupId];
 | 
						|
			}
 | 
						|
		});
 | 
						|
	};
 | 
						|
 | 
						|
	return (
 | 
						|
		<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
 | 
						|
			<div className="bg-white dark:bg-secondary-800 rounded-lg p-6 w-full max-w-md">
 | 
						|
				<div className="flex justify-between items-center mb-4">
 | 
						|
					<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
 | 
						|
						Assign to Host Groups
 | 
						|
					</h3>
 | 
						|
					<button
 | 
						|
						type="button"
 | 
						|
						onClick={onClose}
 | 
						|
						className="text-secondary-400 hover:text-secondary-600 dark:text-secondary-300 dark:hover:text-secondary-100"
 | 
						|
					>
 | 
						|
						<X className="h-5 w-5" />
 | 
						|
					</button>
 | 
						|
				</div>
 | 
						|
 | 
						|
				<div className="mb-4">
 | 
						|
					<p className="text-sm text-secondary-600 dark:text-secondary-400 mb-2">
 | 
						|
						Assigning {selectedHosts.length} host
 | 
						|
						{selectedHosts.length !== 1 ? "s" : ""}:
 | 
						|
					</p>
 | 
						|
					<div className="max-h-32 overflow-y-auto bg-secondary-50 dark:bg-secondary-700 rounded-md p-3">
 | 
						|
						{selectedHostNames.map((friendlyName) => (
 | 
						|
							<div
 | 
						|
								key={friendlyName}
 | 
						|
								className="text-sm text-secondary-700 dark:text-secondary-300"
 | 
						|
							>
 | 
						|
								• {friendlyName}
 | 
						|
							</div>
 | 
						|
						))}
 | 
						|
					</div>
 | 
						|
				</div>
 | 
						|
 | 
						|
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
						|
					<div>
 | 
						|
						<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-3">
 | 
						|
							Host Groups
 | 
						|
						</span>
 | 
						|
						<div className="space-y-2 max-h-48 overflow-y-auto">
 | 
						|
							{/* Host Group Options */}
 | 
						|
							{hostGroups?.map((group) => (
 | 
						|
								<label
 | 
						|
									key={group.id}
 | 
						|
									className={`flex items-center gap-3 p-3 border-2 rounded-lg transition-all duration-200 cursor-pointer ${
 | 
						|
										selectedGroupIds.includes(group.id)
 | 
						|
											? "border-primary-500 bg-primary-50 dark:bg-primary-900/30"
 | 
						|
											: "border-secondary-300 dark:border-secondary-600 bg-white dark:bg-secondary-700 hover:border-secondary-400 dark:hover:border-secondary-500"
 | 
						|
									}`}
 | 
						|
								>
 | 
						|
									<input
 | 
						|
										type="checkbox"
 | 
						|
										checked={selectedGroupIds.includes(group.id)}
 | 
						|
										onChange={() => toggleGroup(group.id)}
 | 
						|
										className="w-4 h-4 text-primary-600 bg-gray-100 border-gray-300 rounded focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
 | 
						|
									/>
 | 
						|
									<div className="flex items-center gap-2 flex-1">
 | 
						|
										{group.color && (
 | 
						|
											<div
 | 
						|
												className="w-3 h-3 rounded-full border border-secondary-300 dark:border-secondary-500 flex-shrink-0"
 | 
						|
												style={{ backgroundColor: group.color }}
 | 
						|
											></div>
 | 
						|
										)}
 | 
						|
										<div className="text-sm font-medium text-secondary-700 dark:text-secondary-200">
 | 
						|
											{group.name}
 | 
						|
										</div>
 | 
						|
									</div>
 | 
						|
								</label>
 | 
						|
							))}
 | 
						|
						</div>
 | 
						|
						<p className="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
 | 
						|
							Select one or more groups to assign these hosts to, or leave
 | 
						|
							ungrouped.
 | 
						|
						</p>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div className="flex justify-end gap-3 pt-4">
 | 
						|
						<button
 | 
						|
							type="button"
 | 
						|
							onClick={onClose}
 | 
						|
							className="btn-outline"
 | 
						|
							disabled={isLoading}
 | 
						|
						>
 | 
						|
							Cancel
 | 
						|
						</button>
 | 
						|
						<button type="submit" className="btn-primary" disabled={isLoading}>
 | 
						|
							{isLoading ? "Assigning..." : "Assign to Groups"}
 | 
						|
						</button>
 | 
						|
					</div>
 | 
						|
				</form>
 | 
						|
			</div>
 | 
						|
		</div>
 | 
						|
	);
 | 
						|
};
 | 
						|
 | 
						|
// Bulk Delete Modal Component
 | 
						|
const BulkDeleteModal = ({
 | 
						|
	selectedHosts,
 | 
						|
	hosts,
 | 
						|
	onClose,
 | 
						|
	onDelete,
 | 
						|
	isLoading,
 | 
						|
}) => {
 | 
						|
	const selectedHostNames = hosts
 | 
						|
		.filter((host) => selectedHosts.includes(host.id))
 | 
						|
		.map((host) => host.friendly_name || host.hostname || host.id);
 | 
						|
 | 
						|
	const handleSubmit = (e) => {
 | 
						|
		e.preventDefault();
 | 
						|
		onDelete();
 | 
						|
	};
 | 
						|
 | 
						|
	return (
 | 
						|
		<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
 | 
						|
			<div className="bg-white dark:bg-secondary-800 rounded-lg shadow-xl max-w-md w-full mx-4">
 | 
						|
				<div className="px-6 py-4 border-b border-secondary-200 dark:border-secondary-600">
 | 
						|
					<div className="flex items-center justify-between">
 | 
						|
						<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
 | 
						|
							Delete Hosts
 | 
						|
						</h3>
 | 
						|
						<button
 | 
						|
							type="button"
 | 
						|
							onClick={onClose}
 | 
						|
							className="text-secondary-400 hover:text-secondary-600 dark:text-secondary-500 dark:hover:text-secondary-300"
 | 
						|
							disabled={isLoading}
 | 
						|
						>
 | 
						|
							<X className="h-5 w-5" />
 | 
						|
						</button>
 | 
						|
					</div>
 | 
						|
				</div>
 | 
						|
 | 
						|
				<div className="px-6 py-4">
 | 
						|
					<div className="mb-4">
 | 
						|
						<div className="flex items-center gap-2 mb-3">
 | 
						|
							<AlertTriangle className="h-5 w-5 text-danger-600" />
 | 
						|
							<h4 className="text-sm font-medium text-danger-800 dark:text-danger-200">
 | 
						|
								Warning: This action cannot be undone
 | 
						|
							</h4>
 | 
						|
						</div>
 | 
						|
						<p className="text-sm text-secondary-600 dark:text-secondary-400 mb-4">
 | 
						|
							You are about to permanently delete {selectedHosts.length} host
 | 
						|
							{selectedHosts.length !== 1 ? "s" : ""}. This will remove all host
 | 
						|
							data, including package information, update history, and API
 | 
						|
							credentials.
 | 
						|
						</p>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div className="mb-4">
 | 
						|
						<p className="text-sm text-secondary-600 dark:text-secondary-400 mb-2">
 | 
						|
							Hosts to be deleted:
 | 
						|
						</p>
 | 
						|
						<div className="max-h-32 overflow-y-auto bg-secondary-50 dark:bg-secondary-700 rounded-md p-3">
 | 
						|
							{selectedHostNames.map((friendlyName) => (
 | 
						|
								<div
 | 
						|
									key={friendlyName}
 | 
						|
									className="text-sm text-secondary-700 dark:text-secondary-300"
 | 
						|
								>
 | 
						|
									• {friendlyName}
 | 
						|
								</div>
 | 
						|
							))}
 | 
						|
						</div>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<form onSubmit={handleSubmit} className="space-y-4">
 | 
						|
						<div className="flex justify-end gap-3 pt-4">
 | 
						|
							<button
 | 
						|
								type="button"
 | 
						|
								onClick={onClose}
 | 
						|
								className="btn-outline"
 | 
						|
								disabled={isLoading}
 | 
						|
							>
 | 
						|
								Cancel
 | 
						|
							</button>
 | 
						|
							<button type="submit" className="btn-danger" disabled={isLoading}>
 | 
						|
								{isLoading
 | 
						|
									? "Deleting..."
 | 
						|
									: `Delete ${selectedHosts.length} Host${selectedHosts.length !== 1 ? "s" : ""}`}
 | 
						|
							</button>
 | 
						|
						</div>
 | 
						|
					</form>
 | 
						|
				</div>
 | 
						|
			</div>
 | 
						|
		</div>
 | 
						|
	);
 | 
						|
};
 | 
						|
 | 
						|
// Column Settings Modal Component
 | 
						|
const ColumnSettingsModal = ({
 | 
						|
	columnConfig,
 | 
						|
	onClose,
 | 
						|
	onToggleVisibility,
 | 
						|
	onReorder,
 | 
						|
	onReset,
 | 
						|
}) => {
 | 
						|
	const [draggedIndex, setDraggedIndex] = useState(null);
 | 
						|
 | 
						|
	const handleDragStart = (e, index) => {
 | 
						|
		setDraggedIndex(index);
 | 
						|
		e.dataTransfer.effectAllowed = "move";
 | 
						|
	};
 | 
						|
 | 
						|
	const handleDragOver = (e) => {
 | 
						|
		e.preventDefault();
 | 
						|
		e.dataTransfer.dropEffect = "move";
 | 
						|
	};
 | 
						|
 | 
						|
	const handleDrop = (e, dropIndex) => {
 | 
						|
		e.preventDefault();
 | 
						|
		if (draggedIndex !== null && draggedIndex !== dropIndex) {
 | 
						|
			onReorder(draggedIndex, dropIndex);
 | 
						|
		}
 | 
						|
		setDraggedIndex(null);
 | 
						|
	};
 | 
						|
 | 
						|
	return (
 | 
						|
		<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
 | 
						|
			<div className="bg-white dark:bg-secondary-800 rounded-lg shadow-xl max-w-lg w-full max-h-[85vh] flex flex-col">
 | 
						|
				{/* Header */}
 | 
						|
				<div className="px-6 py-4 border-b border-secondary-200 dark:border-secondary-600 flex-shrink-0">
 | 
						|
					<div className="flex items-center justify-between">
 | 
						|
						<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
 | 
						|
							Column Settings
 | 
						|
						</h3>
 | 
						|
						<button
 | 
						|
							type="button"
 | 
						|
							onClick={onClose}
 | 
						|
							className="text-secondary-400 hover:text-secondary-600 dark:text-secondary-500 dark:hover:text-secondary-300"
 | 
						|
						>
 | 
						|
							<X className="h-5 w-5" />
 | 
						|
						</button>
 | 
						|
					</div>
 | 
						|
					<p className="text-sm text-secondary-600 dark:text-secondary-300 mt-2">
 | 
						|
						Drag to reorder columns or toggle visibility
 | 
						|
					</p>
 | 
						|
				</div>
 | 
						|
 | 
						|
				{/* Scrollable content */}
 | 
						|
				<div className="px-6 py-4 flex-1 overflow-y-auto">
 | 
						|
					<div className="space-y-1">
 | 
						|
						{columnConfig.map((column, index) => (
 | 
						|
							<button
 | 
						|
								key={column.id}
 | 
						|
								type="button"
 | 
						|
								draggable
 | 
						|
								tabIndex={0}
 | 
						|
								aria-label={`Drag to reorder ${column.label} column`}
 | 
						|
								onDragStart={(e) => handleDragStart(e, index)}
 | 
						|
								onDragOver={handleDragOver}
 | 
						|
								onDrop={(e) => handleDrop(e, index)}
 | 
						|
								onKeyDown={(e) => {
 | 
						|
									if (e.key === "Enter" || e.key === " ") {
 | 
						|
										e.preventDefault();
 | 
						|
										// Focus handling for keyboard users
 | 
						|
									}
 | 
						|
								}}
 | 
						|
								className={`flex items-center justify-between p-2.5 border rounded-lg cursor-move w-full text-left transition-colors ${
 | 
						|
									draggedIndex === index
 | 
						|
										? "opacity-50"
 | 
						|
										: "hover:bg-secondary-50 dark:hover:bg-secondary-700"
 | 
						|
								} border-secondary-200 dark:border-secondary-600`}
 | 
						|
							>
 | 
						|
								<div className="flex items-center gap-2.5">
 | 
						|
									<GripVertical className="h-4 w-4 text-secondary-400 dark:text-secondary-500 flex-shrink-0" />
 | 
						|
									<span className="text-sm font-medium text-secondary-900 dark:text-white truncate">
 | 
						|
										{column.label}
 | 
						|
									</span>
 | 
						|
								</div>
 | 
						|
								<button
 | 
						|
									type="button"
 | 
						|
									onClick={() => onToggleVisibility(column.id)}
 | 
						|
									className={`p-1 rounded transition-colors flex-shrink-0 ${
 | 
						|
										column.visible
 | 
						|
											? "text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
 | 
						|
											: "text-secondary-400 hover:text-secondary-600 dark:text-secondary-500 dark:hover:text-secondary-300"
 | 
						|
									}`}
 | 
						|
								>
 | 
						|
									{column.visible ? (
 | 
						|
										<EyeIcon className="h-4 w-4" />
 | 
						|
									) : (
 | 
						|
										<EyeOffIcon className="h-4 w-4" />
 | 
						|
									)}
 | 
						|
								</button>
 | 
						|
							</button>
 | 
						|
						))}
 | 
						|
					</div>
 | 
						|
				</div>
 | 
						|
 | 
						|
				{/* Footer */}
 | 
						|
				<div className="px-6 py-4 border-t border-secondary-200 dark:border-secondary-600 flex-shrink-0">
 | 
						|
					<div className="flex justify-between">
 | 
						|
						<button type="button" onClick={onReset} className="btn-outline">
 | 
						|
							Reset to Default
 | 
						|
						</button>
 | 
						|
						<button type="button" onClick={onClose} className="btn-primary">
 | 
						|
							Done
 | 
						|
						</button>
 | 
						|
					</div>
 | 
						|
				</div>
 | 
						|
			</div>
 | 
						|
		</div>
 | 
						|
	);
 | 
						|
};
 | 
						|
 | 
						|
export default Hosts;
 |