mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-09 16:37:29 +00:00
fix(frontend): variable unused
This commit is contained in:
@@ -9,11 +9,8 @@ import {
|
||||
ChevronDown,
|
||||
Clock,
|
||||
Columns,
|
||||
Copy,
|
||||
ExternalLink,
|
||||
Eye,
|
||||
Eye as EyeIcon,
|
||||
EyeOff,
|
||||
EyeOff as EyeOffIcon,
|
||||
Filter,
|
||||
GripVertical,
|
||||
@@ -35,7 +32,6 @@ import {
|
||||
dashboardAPI,
|
||||
formatRelativeTime,
|
||||
hostGroupsAPI,
|
||||
settingsAPI,
|
||||
} from "../utils/api";
|
||||
import { OSIcon } from "../utils/osIcons.jsx";
|
||||
|
||||
@@ -232,565 +228,6 @@ const AddHostModal = ({ isOpen, onClose, onSuccess }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// Credentials Modal Component
|
||||
const CredentialsModal = ({ host, isOpen, onClose }) => {
|
||||
const apiIdId = useId();
|
||||
const apiKeyId = useId();
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
host?.isNewHost ? "quick" : "credentials",
|
||||
);
|
||||
|
||||
// Update active tab when host changes
|
||||
React.useEffect(() => {
|
||||
if (host?.isNewHost) {
|
||||
setActiveTab("quick");
|
||||
} else {
|
||||
setActiveTab("credentials");
|
||||
}
|
||||
}, [host?.isNewHost]);
|
||||
|
||||
const copyToClipboard = async (text, label) => {
|
||||
try {
|
||||
// Try modern clipboard API first
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
alert(`${label} copied to clipboard!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback for older browsers or non-secure contexts
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.left = "-999999px";
|
||||
textArea.style.top = "-999999px";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand("copy");
|
||||
if (successful) {
|
||||
alert(`${label} copied to clipboard!`);
|
||||
} else {
|
||||
throw new Error("Copy command failed");
|
||||
}
|
||||
} catch {
|
||||
// If all else fails, show the text in a prompt
|
||||
prompt(`Copy this ${label.toLowerCase()}:`, text);
|
||||
} finally {
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to copy to clipboard:", err);
|
||||
// Show the text in a prompt as last resort
|
||||
prompt(`Copy this ${label.toLowerCase()}:`, text);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch server URL from settings
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ["settings"],
|
||||
queryFn: () => settingsAPI.get().then((res) => res.data),
|
||||
enabled: isOpen, // Only fetch when modal is open
|
||||
});
|
||||
|
||||
const serverUrl =
|
||||
settings?.server_url || window.location.origin.replace(":3000", ":3001");
|
||||
|
||||
const getSetupCommands = () => {
|
||||
// Get current time for crontab scheduling
|
||||
const now = new Date();
|
||||
const currentMinute = now.getMinutes();
|
||||
const currentHour = now.getHours();
|
||||
|
||||
return {
|
||||
oneLine: `curl -sSL ${serverUrl}/api/v1/hosts/install | bash -s -- ${serverUrl} "${host?.api_id}" "${host?.api_key}"`,
|
||||
|
||||
download: `# Download and setup PatchMon agent
|
||||
curl -o /tmp/patchmon-agent.sh ${serverUrl}/api/v1/hosts/agent/download
|
||||
sudo mkdir -p /etc/patchmon
|
||||
sudo mv /tmp/patchmon-agent.sh /usr/local/bin/patchmon-agent.sh
|
||||
sudo chmod +x /usr/local/bin/patchmon-agent.sh`,
|
||||
|
||||
configure: `# Configure API credentials
|
||||
sudo /usr/local/bin/patchmon-agent.sh configure "${host?.api_id}" "${host?.api_key}"`,
|
||||
|
||||
test: `# Test the configuration
|
||||
sudo /usr/local/bin/patchmon-agent.sh test`,
|
||||
|
||||
initialUpdate: `# Send initial package data
|
||||
sudo /usr/local/bin/patchmon-agent.sh update`,
|
||||
|
||||
crontab: `# Add to crontab for hourly updates starting at current time (run as root)
|
||||
echo "${currentMinute} * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" | sudo crontab -`,
|
||||
|
||||
fullSetup: `#!/bin/bash
|
||||
# Complete PatchMon Agent Setup Script
|
||||
# Run this on the target host: ${host?.friendly_name}
|
||||
|
||||
echo "🔄 Setting up PatchMon agent..."
|
||||
|
||||
# Download and install agent
|
||||
echo "📥 Downloading agent script..."
|
||||
curl -o /tmp/patchmon-agent.sh ${serverUrl}/api/v1/hosts/agent/download
|
||||
sudo mkdir -p /etc/patchmon
|
||||
sudo mv /tmp/patchmon-agent.sh /usr/local/bin/patchmon-agent.sh
|
||||
sudo chmod +x /usr/local/bin/patchmon-agent.sh
|
||||
|
||||
# Configure credentials
|
||||
echo "🔑 Configuring API credentials..."
|
||||
sudo /usr/local/bin/patchmon-agent.sh configure "${host?.api_id}" "${host?.api_key}"
|
||||
|
||||
# Test configuration
|
||||
echo "🧪 Testing configuration..."
|
||||
sudo /usr/local/bin/patchmon-agent.sh test
|
||||
|
||||
# Send initial update
|
||||
echo "📊 Sending initial package data..."
|
||||
sudo /usr/local/bin/patchmon-agent.sh update
|
||||
|
||||
# Setup crontab starting at current time
|
||||
echo "⏰ Setting up hourly crontab starting at ${currentHour.toString().padStart(2, "0")}:${currentMinute.toString().padStart(2, "0")}..."
|
||||
echo "${currentMinute} * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" | sudo crontab -
|
||||
|
||||
echo "✅ PatchMon agent setup complete!"
|
||||
echo " - Agent installed: /usr/local/bin/patchmon-agent.sh"
|
||||
echo " - Config directory: /etc/patchmon/"
|
||||
echo " - Updates: Every hour via crontab (starting at ${currentHour.toString().padStart(2, "0")}:${currentMinute.toString().padStart(2, "0")})"
|
||||
echo " - View logs: tail -f /var/log/patchmon-agent.log"`,
|
||||
};
|
||||
};
|
||||
|
||||
if (!isOpen || !host) return null;
|
||||
|
||||
const commands = getSetupCommands();
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-secondary-900">
|
||||
Host Setup - {host.friendly_name}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="text-secondary-400 hover:text-secondary-600"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex space-x-1 mb-6 bg-secondary-100 p-1 rounded-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("quick")}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
activeTab === "quick"
|
||||
? "bg-white text-secondary-900 shadow-sm"
|
||||
: "text-secondary-600 hover:text-secondary-900"
|
||||
}`}
|
||||
>
|
||||
Quick Install
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("credentials")}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
activeTab === "credentials"
|
||||
? "bg-white text-secondary-900 shadow-sm"
|
||||
: "text-secondary-600 hover:text-secondary-900"
|
||||
}`}
|
||||
>
|
||||
API Credentials
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("setup")}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
activeTab === "setup"
|
||||
? "bg-white text-secondary-900 shadow-sm"
|
||||
: "text-secondary-600 hover:text-secondary-900"
|
||||
}`}
|
||||
>
|
||||
Setup Instructions
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("script")}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
activeTab === "script"
|
||||
? "bg-white text-secondary-900 shadow-sm"
|
||||
: "text-secondary-600 hover:text-secondary-900"
|
||||
}`}
|
||||
>
|
||||
Auto-Setup Script
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === "quick" && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-green-50 border border-green-200 rounded-md p-4">
|
||||
<h4 className="text-sm font-medium text-green-800 mb-2">
|
||||
🚀 One-Line Installation
|
||||
</h4>
|
||||
<p className="text-sm text-green-700">
|
||||
Copy and paste this single command on{" "}
|
||||
<strong>{host.friendly_name}</strong> to install and configure
|
||||
the PatchMon agent automatically.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border border-secondary-200 rounded-lg p-4">
|
||||
<h5 className="font-medium text-secondary-900 mb-3">
|
||||
Installation Command
|
||||
</h5>
|
||||
<div className="bg-secondary-900 text-secondary-100 p-3 rounded font-mono text-sm">
|
||||
<div className="flex justify-between items-start">
|
||||
<code className="flex-1 whitespace-pre-wrap break-all">
|
||||
{commands.oneLine}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(commands.oneLine, "Installation command")
|
||||
}
|
||||
className="ml-2 text-secondary-400 hover:text-secondary-200 flex-shrink-0"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-secondary-500 mt-2">
|
||||
This command will download, install, configure, and set up
|
||||
automatic updates for the PatchMon agent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-md p-4">
|
||||
<h4 className="text-sm font-medium text-blue-800 mb-2">
|
||||
📋 What This Command Does
|
||||
</h4>
|
||||
<ul className="text-sm text-blue-700 space-y-1">
|
||||
<li>• Downloads the PatchMon installation script</li>
|
||||
<li>
|
||||
• Installs the agent to{" "}
|
||||
<code>/usr/local/bin/patchmon-agent.sh</code>
|
||||
</li>
|
||||
<li>
|
||||
• Configures API credentials for{" "}
|
||||
<strong>{host.friendly_name}</strong>
|
||||
</li>
|
||||
<li>• Tests the connection to PatchMon server</li>
|
||||
<li>• Sends initial package data</li>
|
||||
<li>• Sets up hourly automatic updates via crontab</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-md p-4">
|
||||
<h4 className="text-sm font-medium text-amber-800 mb-2">
|
||||
⚠️ Requirements
|
||||
</h4>
|
||||
<ul className="text-sm text-amber-700 space-y-1">
|
||||
<li>
|
||||
• Must be run as root (use <code>sudo</code>)
|
||||
</li>
|
||||
<li>• Requires internet connection to download agent</li>
|
||||
<li>
|
||||
• Requires <code>curl</code> and <code>bash</code> to be
|
||||
installed
|
||||
</li>
|
||||
<li>• Host must be able to reach the PatchMon server</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "credentials" && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor={apiIdId}
|
||||
className="block text-sm font-medium text-secondary-700 mb-2"
|
||||
>
|
||||
API ID
|
||||
</label>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
id={apiIdId}
|
||||
readOnly
|
||||
value={host.apiId}
|
||||
className="flex-1 block w-full border-secondary-300 rounded-l-md shadow-sm bg-secondary-50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(host.apiId, "API ID")}
|
||||
className="px-3 py-2 border border-l-0 border-secondary-300 rounded-r-md bg-secondary-50 hover:bg-secondary-100"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor={apiKeyId}
|
||||
className="block text-sm font-medium text-secondary-700 mb-2"
|
||||
>
|
||||
API Key
|
||||
</label>
|
||||
<div className="flex">
|
||||
<input
|
||||
type={showApiKey ? "text" : "password"}
|
||||
id={apiKeyId}
|
||||
readOnly
|
||||
value={host.apiKey}
|
||||
className="flex-1 block w-full border-secondary-300 rounded-l-md shadow-sm bg-secondary-50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowApiKey(!showApiKey)}
|
||||
className="px-3 py-2 border border-l-0 border-r-0 border-secondary-300 bg-secondary-50 hover:bg-secondary-100"
|
||||
>
|
||||
{showApiKey ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(host.apiKey, "API Key")}
|
||||
className="px-3 py-2 border border-l-0 border-secondary-300 rounded-r-md bg-secondary-50 hover:bg-secondary-100"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-md p-4">
|
||||
<h4 className="text-sm font-medium text-amber-800 mb-2">
|
||||
⚠️ Security Note
|
||||
</h4>
|
||||
<p className="text-sm text-amber-700">
|
||||
Keep these credentials secure. They provide access to update
|
||||
package information for <strong>{host.friendly_name}</strong>{" "}
|
||||
only.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "setup" && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-md p-4">
|
||||
<h4 className="text-sm font-medium text-blue-800 mb-2">
|
||||
📋 Step-by-Step Setup
|
||||
</h4>
|
||||
<p className="text-sm text-blue-700">
|
||||
Follow these commands on <strong>{host.friendly_name}</strong>{" "}
|
||||
to install and configure the PatchMon agent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Step 1: Download & Install */}
|
||||
<div className="border border-secondary-200 rounded-lg p-4">
|
||||
<h5 className="font-medium text-secondary-900 mb-3">
|
||||
Step 1: Download & Install Agent
|
||||
</h5>
|
||||
<div className="bg-secondary-900 text-secondary-100 p-3 rounded font-mono text-sm">
|
||||
<div className="flex justify-between items-start">
|
||||
<code className="flex-1 whitespace-pre-wrap">
|
||||
{commands.download}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(commands.download, "Download commands")
|
||||
}
|
||||
className="ml-2 text-secondary-400 hover:text-secondary-200 flex-shrink-0"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 2: Configure */}
|
||||
<div className="border border-secondary-200 rounded-lg p-4">
|
||||
<h5 className="font-medium text-secondary-900 mb-3">
|
||||
Step 2: Configure API Credentials
|
||||
</h5>
|
||||
<div className="bg-secondary-900 text-secondary-100 p-3 rounded font-mono text-sm">
|
||||
<div className="flex justify-between items-start">
|
||||
<code className="flex-1">{commands.configure}</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(commands.configure, "Configure command")
|
||||
}
|
||||
className="ml-2 text-secondary-400 hover:text-secondary-200"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 3: Test */}
|
||||
<div className="border border-secondary-200 rounded-lg p-4">
|
||||
<h5 className="font-medium text-secondary-900 mb-3">
|
||||
Step 3: Test Configuration
|
||||
</h5>
|
||||
<div className="bg-secondary-900 text-secondary-100 p-3 rounded font-mono text-sm">
|
||||
<div className="flex justify-between items-start">
|
||||
<code className="flex-1">{commands.test}</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(commands.test, "Test command")
|
||||
}
|
||||
className="ml-2 text-secondary-400 hover:text-secondary-200"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 4: Initial Update */}
|
||||
<div className="border border-secondary-200 rounded-lg p-4">
|
||||
<h5 className="font-medium text-secondary-900 mb-3">
|
||||
Step 4: Send Initial Package Data
|
||||
</h5>
|
||||
<p className="text-sm text-secondary-600 mb-3">
|
||||
This will automatically detect and send system information (OS,
|
||||
IP, architecture) along with package data.
|
||||
</p>
|
||||
<div className="bg-secondary-900 text-secondary-100 p-3 rounded font-mono text-sm">
|
||||
<div className="flex justify-between items-start">
|
||||
<code className="flex-1">{commands.initialUpdate}</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
commands.initialUpdate,
|
||||
"Initial update command",
|
||||
)
|
||||
}
|
||||
className="ml-2 text-secondary-400 hover:text-secondary-200"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 5: Crontab */}
|
||||
<div className="border border-secondary-200 rounded-lg p-4">
|
||||
<h5 className="font-medium text-secondary-900 mb-3">
|
||||
Step 5: Setup Hourly Updates
|
||||
</h5>
|
||||
<div className="bg-secondary-900 text-secondary-100 p-3 rounded font-mono text-sm">
|
||||
<div className="flex justify-between items-start">
|
||||
<code className="flex-1">{commands.crontab}</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(commands.crontab, "Crontab command")
|
||||
}
|
||||
className="ml-2 text-secondary-400 hover:text-secondary-200"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-secondary-500 mt-2">
|
||||
This sets up automatic package updates every hour at the top of
|
||||
the hour.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "script" && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-green-50 border border-green-200 rounded-md p-4">
|
||||
<h4 className="text-sm font-medium text-green-800 mb-2">
|
||||
🚀 Automated Setup
|
||||
</h4>
|
||||
<p className="text-sm text-green-700">
|
||||
Copy this complete setup script to{" "}
|
||||
<strong>{host.friendly_name}</strong> and run it to
|
||||
automatically install and configure everything.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border border-secondary-200 rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h5 className="font-medium text-secondary-900">
|
||||
Complete Setup Script
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyToClipboard(commands.fullSetup, "Complete setup script")
|
||||
}
|
||||
className="px-3 py-1 bg-primary-600 text-white rounded text-sm hover:bg-primary-700 flex items-center gap-2"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy Script
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-secondary-900 text-secondary-100 p-4 rounded font-mono text-xs overflow-x-auto">
|
||||
<pre className="whitespace-pre-wrap">{commands.fullSetup}</pre>
|
||||
</div>
|
||||
<div className="mt-3 text-sm text-secondary-600">
|
||||
<p>
|
||||
<strong>Usage:</strong>
|
||||
</p>
|
||||
<p>1. Copy the script above</p>
|
||||
<p>
|
||||
2. Save it to a file on {host.friendly_name} (e.g.,{" "}
|
||||
<code>setup-patchmon.sh</code>)
|
||||
</p>
|
||||
<p>
|
||||
3. Run:{" "}
|
||||
<code>
|
||||
chmod +x setup-patchmon.sh && sudo ./setup-patchmon.sh
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex justify-end space-x-3">
|
||||
<a
|
||||
href={`${serverUrl}/api/v1/hosts/agent/download`}
|
||||
download="patchmon-agent.sh"
|
||||
className="px-4 py-2 text-sm font-medium text-primary-700 bg-primary-50 border border-primary-200 rounded-md hover:bg-primary-100"
|
||||
>
|
||||
Download Agent Script
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-secondary-700 bg-white border border-secondary-300 rounded-md hover:bg-secondary-50"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Hosts = () => {
|
||||
const hostGroupFilterId = useId();
|
||||
const statusFilterId = useId();
|
||||
|
||||
Reference in New Issue
Block a user