Align settings cards and add manual host install
This commit is contained in:
parent
a6bf2c852b
commit
c933eb699c
2 changed files with 177 additions and 77 deletions
|
|
@ -365,94 +365,94 @@ WantedBy=multi-user.target`;
|
|||
};
|
||||
|
||||
return (
|
||||
<div class="space-y-8">
|
||||
<Card class="space-y-5">
|
||||
<div>
|
||||
<div class="space-y-6">
|
||||
<Card padding="lg" class="space-y-5">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Add a Docker host</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Run this command as root on your Docker host to start monitoring.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Show when={requiresToken()}>
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">Generate API token</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Create a fresh token scoped to <code>{DOCKER_REPORT_SCOPE}</code>
|
||||
<div class="space-y-5">
|
||||
<Show when={requiresToken()}>
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">Generate API token</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Create a fresh token scoped to <code>{DOCKER_REPORT_SCOPE}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={tokenName()}
|
||||
onInput={(e) => setTokenName(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !isGeneratingToken()) {
|
||||
handleGenerateToken();
|
||||
}
|
||||
}}
|
||||
placeholder="Token name (optional)"
|
||||
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:ring-blue-900/60"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateToken}
|
||||
disabled={isGeneratingToken()}
|
||||
class="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{isGeneratingToken() ? 'Generating…' : currentToken() ? 'Generate another' : 'Generate token'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={latestRecord()}>
|
||||
<div class="flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-4 py-2 text-xs text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span>
|
||||
Token <strong>{latestRecord()?.name}</strong> created and inserted into the command below.
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={showInstallCommand()}>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-sm font-semibold text-gray-900 dark:text-gray-100">Install command</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
const command = getInstallCommandTemplate().replace(TOKEN_PLACEHOLDER, currentToken() || TOKEN_PLACEHOLDER);
|
||||
const success = await copyToClipboard(command);
|
||||
if (typeof window !== 'undefined' && window.showToast) {
|
||||
window.showToast(success ? 'success' : 'error', success ? 'Copied!' : 'Failed to copy');
|
||||
}
|
||||
}}
|
||||
class="px-3 py-1.5 text-xs font-medium rounded-lg transition-colors bg-blue-600 text-white hover:bg-blue-700"
|
||||
>
|
||||
Copy command
|
||||
</button>
|
||||
</div>
|
||||
<pre class="overflow-x-auto rounded-md bg-gray-900/90 p-3 text-xs text-gray-100">
|
||||
<code>{getInstallCommandTemplate().replace(TOKEN_PLACEHOLDER, currentToken() || TOKEN_PLACEHOLDER)}</code>
|
||||
</pre>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Run as root on your Docker host. The installer downloads the agent, creates a systemd service, and starts reporting automatically.
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={tokenName()}
|
||||
onInput={(e) => setTokenName(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !isGeneratingToken()) {
|
||||
handleGenerateToken();
|
||||
}
|
||||
}}
|
||||
placeholder="Token name (optional)"
|
||||
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:ring-blue-900/60"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateToken}
|
||||
disabled={isGeneratingToken()}
|
||||
class="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{isGeneratingToken() ? 'Generating…' : currentToken() ? 'Generate another' : 'Generate token'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={latestRecord()}>
|
||||
<div class="flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 px-4 py-2 text-xs text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-200">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span>
|
||||
Token <strong>{latestRecord()?.name}</strong> created and inserted into the command below.
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={showInstallCommand()}>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-sm font-semibold text-gray-900 dark:text-gray-100">Install command</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
const command = getInstallCommandTemplate().replace(TOKEN_PLACEHOLDER, currentToken() || TOKEN_PLACEHOLDER);
|
||||
const success = await copyToClipboard(command);
|
||||
if (typeof window !== 'undefined' && window.showToast) {
|
||||
window.showToast(success ? 'success' : 'error', success ? 'Copied!' : 'Failed to copy');
|
||||
}
|
||||
}}
|
||||
class="px-3 py-1.5 text-xs font-medium rounded-lg transition-colors bg-blue-600 text-white hover:bg-blue-700"
|
||||
>
|
||||
Copy command
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 p-3 overflow-x-auto">
|
||||
<code class="text-sm text-gray-900 dark:text-gray-100 font-mono break-all">
|
||||
{getInstallCommandTemplate().replace(TOKEN_PLACEHOLDER, currentToken() || TOKEN_PLACEHOLDER)}
|
||||
</code>
|
||||
</div>
|
||||
<Show when={requiresToken() && !currentToken()}>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Run as root on your Docker host. The installer downloads the agent, creates a systemd service, and starts reporting automatically.
|
||||
Generate a token to see the install command.
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={requiresToken() && !currentToken()}>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Generate a token to see the install command.
|
||||
</p>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<details class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-700 dark:bg-gray-800/50 dark:text-gray-300">
|
||||
<summary class="cursor-pointer text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
|
|
|
|||
|
|
@ -234,6 +234,28 @@ export const HostAgents: Component = () => {
|
|||
return currentToken() || 'disabled';
|
||||
};
|
||||
|
||||
const getSystemdServiceUnit = () => `[Unit]
|
||||
Description=Pulse Host Agent
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/pulse-host-agent --url ${pulseUrl()} --token ${resolvedToken()} --interval 30s
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
User=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target`;
|
||||
|
||||
const getManualUninstallCommand = () =>
|
||||
`sudo systemctl stop pulse-host-agent && \\
|
||||
sudo systemctl disable pulse-host-agent && \\
|
||||
sudo rm -f /etc/systemd/system/pulse-host-agent.service && \\
|
||||
sudo rm -f /usr/local/bin/pulse-host-agent && \\
|
||||
sudo systemctl daemon-reload`;
|
||||
|
||||
return (
|
||||
<div class="space-y-6">
|
||||
<Card padding="lg" class="space-y-5">
|
||||
|
|
@ -365,6 +387,84 @@ export const HostAgents: Component = () => {
|
|||
)}
|
||||
</For>
|
||||
</div>
|
||||
<details class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-700 dark:bg-gray-800/50 dark:text-gray-300">
|
||||
<summary class="cursor-pointer text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Advanced options (manual install & uninstall)
|
||||
</summary>
|
||||
<div class="mt-3 space-y-4">
|
||||
<div class="space-y-3 rounded-lg border border-gray-200 bg-white p-3 text-xs dark:border-gray-700 dark:bg-gray-900">
|
||||
<p class="font-medium text-gray-900 dark:text-gray-100">Manual Linux install</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Build the agent from source and manage the service yourself instead of using the helper script.
|
||||
</p>
|
||||
<p class="font-medium text-gray-900 dark:text-gray-100">1. Build the binary</p>
|
||||
<div class="rounded bg-gray-900 p-3 font-mono text-xs text-gray-100 dark:bg-gray-950">
|
||||
<code>
|
||||
cd /opt/pulse
|
||||
<br />
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o pulse-host-agent ./cmd/pulse-host-agent
|
||||
</code>
|
||||
</div>
|
||||
<p class="font-medium text-gray-900 dark:text-gray-100">2. Copy to host</p>
|
||||
<div class="rounded bg-gray-900 p-3 font-mono text-xs text-gray-100 dark:bg-gray-950">
|
||||
<code>
|
||||
scp pulse-host-agent user@host:/usr/local/bin/
|
||||
<br />
|
||||
ssh user@host sudo chmod +x /usr/local/bin/pulse-host-agent
|
||||
</code>
|
||||
</div>
|
||||
<p class="font-medium text-gray-900 dark:text-gray-100">3. Systemd service template</p>
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
const success = await copyToClipboard(getSystemdServiceUnit());
|
||||
if (typeof window !== 'undefined' && window.showToast) {
|
||||
window.showToast(success ? 'success' : 'error', success ? 'Copied to clipboard' : 'Failed to copy to clipboard');
|
||||
}
|
||||
}}
|
||||
class="absolute right-2 top-2 rounded-lg bg-gray-700 px-3 py-1.5 text-xs font-medium text-gray-200 transition-colors hover:bg-gray-600"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<div class="rounded bg-gray-900 p-3 font-mono text-xs text-gray-100 dark:bg-gray-950 overflow-x-auto">
|
||||
<pre>{getSystemdServiceUnit()}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-medium text-gray-900 dark:text-gray-100">4. Enable & start</p>
|
||||
<div class="rounded bg-gray-900 p-3 font-mono text-xs text-gray-100 dark:bg-gray-950">
|
||||
<code>
|
||||
sudo systemctl daemon-reload
|
||||
<br />
|
||||
sudo systemctl enable --now pulse-host-agent
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">Manual uninstall</p>
|
||||
<div class="mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||||
<code class="flex-1 break-all rounded bg-gray-900 px-3 py-2 font-mono text-xs text-gray-100 dark:bg-gray-950">
|
||||
{getManualUninstallCommand()}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
const success = await copyToClipboard(getManualUninstallCommand());
|
||||
if (typeof window !== 'undefined' && window.showToast) {
|
||||
window.showToast(success ? 'success' : 'error', success ? 'Copied to clipboard' : 'Failed to copy to clipboard');
|
||||
}
|
||||
}}
|
||||
class="self-start rounded-lg bg-red-50 px-3 py-1.5 text-xs font-medium text-red-700 transition-colors hover:bg-red-100 dark:bg-red-900/30 dark:text-red-300 dark:hover:bg-red-900/50"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Stops the agent, removes the systemd unit, and deletes the binary.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue