- Replace phx:copy event with CopyToClipboard LiveView hook for more reliable clipboard functionality - Add visual feedback (checkmark) when text is copied to clipboard - Apply hook to both agent token and docker compose copy buttons - Add readonly attribute to docker compose textarea - Fix org settings page to stay on settings after save instead of redirecting to dashboard The clipboard copy now works correctly by using a dedicated LiveView hook that directly handles the click event and clipboard API, rather than relying on custom event dispatching which was causing issues.
313 lines
12 KiB
Text
313 lines
12 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
current_organization={@current_organization}
|
|
active_page="agents"
|
|
>
|
|
<.header>
|
|
{@page_title}
|
|
<:subtitle>Manage remote agents for local SNMP polling</:subtitle>
|
|
<:actions>
|
|
<.button phx-click={JS.show(to: "#new-agent-form")} variant="primary">
|
|
<.icon name="hero-plus" class="h-5 w-5" /> New Agent
|
|
</.button>
|
|
</:actions>
|
|
</.header>
|
|
|
|
<!-- New Agent Form -->
|
|
<div id="new-agent-form" class="hidden mt-6">
|
|
<div class="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg p-6">
|
|
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-4">
|
|
Create New Agent
|
|
</h3>
|
|
<.form for={@agent_form} phx-submit="create_agent">
|
|
<div class="space-y-4">
|
|
<.input
|
|
field={@agent_form[:name]}
|
|
type="text"
|
|
label="Agent Name"
|
|
placeholder="e.g., Datacenter A"
|
|
required
|
|
/>
|
|
<div class="flex gap-3">
|
|
<.button type="submit" variant="primary">
|
|
Create Agent
|
|
</.button>
|
|
<button
|
|
type="button"
|
|
phx-click={JS.hide(to: "#new-agent-form")}
|
|
class="px-3 py-2 text-sm font-semibold text-zinc-700 dark:text-zinc-300 hover:text-zinc-900 dark:hover:text-zinc-100"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</.form>
|
|
</div>
|
|
</div>
|
|
|
|
<%= if @agent_tokens == [] do %>
|
|
<div class="text-center py-16">
|
|
<.icon name="hero-server" class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-600" />
|
|
<h3 class="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">No agents</h3>
|
|
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
|
Get started by creating your first remote agent.
|
|
</p>
|
|
<div class="mt-6">
|
|
<.button phx-click={JS.show(to: "#new-agent-form")} variant="primary">
|
|
<.icon name="hero-plus" class="h-5 w-5" /> New Agent
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<div class="mt-6">
|
|
<.table id="agents-table" rows={@agent_tokens}>
|
|
<:col :let={agent} label="Name">
|
|
<.link
|
|
navigate={~p"/orgs/#{@current_organization.slug}/agents/#{agent.id}"}
|
|
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
|
>
|
|
{agent.name}
|
|
</.link>
|
|
<%= if agent.metadata["version"] do %>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
|
v{agent.metadata["version"]}
|
|
</div>
|
|
<% end %>
|
|
</:col>
|
|
|
|
<:col :let={agent} label="Status">
|
|
<% {status, label} = agent_status(agent) %>
|
|
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
|
|
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
|
|
{label}
|
|
</span>
|
|
</:col>
|
|
|
|
<:col :let={agent} label="Equipment">
|
|
<% counts = Map.get(@equipment_counts, agent.id, %{direct: 0, total: 0}) %>
|
|
<div class="text-sm text-zinc-900 dark:text-zinc-100">
|
|
{counts.total} total
|
|
</div>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
|
{counts.direct} direct
|
|
<%= if counts.total > counts.direct do %>
|
|
· {counts.total - counts.direct} inherited
|
|
<% end %>
|
|
</div>
|
|
</:col>
|
|
|
|
<:col :let={agent} label="Last Seen">
|
|
<div class="text-sm text-zinc-900 dark:text-zinc-100">
|
|
{format_last_seen(agent.last_seen_at)}
|
|
</div>
|
|
<%= if agent.last_seen_at do %>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
|
{format_datetime(agent.last_seen_at)}
|
|
</div>
|
|
<% end %>
|
|
<%= if agent.last_ip do %>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5 font-mono">
|
|
{agent.last_ip}
|
|
</div>
|
|
<% end %>
|
|
</:col>
|
|
|
|
<:col :let={agent} label="Metadata">
|
|
<%= if agent.metadata["hostname"] do %>
|
|
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
|
<.icon name="hero-computer-desktop" class="h-4 w-4 inline" />
|
|
{agent.metadata["hostname"]}
|
|
</div>
|
|
<% end %>
|
|
</:col>
|
|
|
|
<:col :let={agent} label="Created">
|
|
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
|
{Calendar.strftime(agent.inserted_at, "%b %d, %Y")}
|
|
</div>
|
|
</:col>
|
|
|
|
<:action :let={agent}>
|
|
<%= if agent.enabled do %>
|
|
<div class="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
phx-click="show_setup"
|
|
phx-value-id={agent.id}
|
|
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
|
>
|
|
View Setup
|
|
</button>
|
|
<button
|
|
type="button"
|
|
phx-click="revoke_agent"
|
|
phx-value-id={agent.id}
|
|
data-confirm="Are you sure you want to revoke this agent? It will no longer be able to authenticate."
|
|
class="text-sm font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
|
>
|
|
Revoke
|
|
</button>
|
|
</div>
|
|
<% else %>
|
|
<span class="text-xs text-zinc-500 dark:text-zinc-400">Revoked</span>
|
|
<% end %>
|
|
</:action>
|
|
</.table>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Token Display Modal -->
|
|
<%= if @show_token_modal && @new_token do %>
|
|
<div
|
|
id="token-modal"
|
|
class="fixed inset-0 z-50 overflow-y-auto"
|
|
phx-mounted={JS.show()}
|
|
>
|
|
<div class="flex min-h-screen items-center justify-center p-4">
|
|
<div
|
|
class="fixed inset-0 bg-zinc-500/75 dark:bg-zinc-950/75 transition-opacity"
|
|
phx-click="close_token_modal"
|
|
>
|
|
</div>
|
|
|
|
<div class="relative bg-white dark:bg-zinc-900 rounded-lg shadow-xl max-w-2xl w-full">
|
|
<div class="p-6">
|
|
<div class="flex items-start justify-between mb-4">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
|
|
Agent Setup Instructions
|
|
</h3>
|
|
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
|
Use the token and Docker Compose configuration below to deploy this agent.
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
phx-click="close_token_modal"
|
|
class="text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300"
|
|
>
|
|
<.icon name="hero-x-mark" class="h-6 w-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
|
|
Agent Name
|
|
</label>
|
|
<div class="text-base font-mono text-zinc-900 dark:text-zinc-100">
|
|
{@new_token.agent_token.name}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
|
|
Authentication Token
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
type="text"
|
|
readonly
|
|
value={@new_token.token}
|
|
class="w-full font-mono text-sm bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded px-3 py-2 pr-24 text-zinc-900 dark:text-zinc-100"
|
|
id="agent-token-input"
|
|
/>
|
|
<button
|
|
type="button"
|
|
phx-hook="CopyToClipboard"
|
|
data-target="#agent-token-input"
|
|
id={"copy-token-#{@new_token.id}"}
|
|
class="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1 text-xs font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
|
>
|
|
<.icon name="hero-clipboard" class="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
|
|
<h4 class="text-sm font-medium text-zinc-900 dark:text-zinc-100 mb-2">
|
|
Docker Compose Setup with Auto-Updates
|
|
</h4>
|
|
<p class="text-xs text-zinc-600 dark:text-zinc-400 mb-3">
|
|
This configuration includes Watchtower for automatic agent updates. The agent will automatically update to the latest version every 15 minutes during development.
|
|
</p>
|
|
<pre class="text-xs font-mono text-zinc-600 dark:text-zinc-400 overflow-x-auto"><code>services:
|
|
towerops-agent:
|
|
image: {@agent_image}
|
|
container_name: towerops-agent
|
|
restart: unless-stopped
|
|
environment:
|
|
- TOWEROPS_API_URL={url(@socket, ~p"/")}
|
|
- TOWEROPS_AGENT_TOKEN={@new_token.token}
|
|
volumes:
|
|
- ./data:/data
|
|
labels:
|
|
- "com.centurylinklabs.watchtower.enable=true"
|
|
- "com.centurylinklabs.watchtower.scope=towerops"
|
|
|
|
watchtower:
|
|
image: containrrr/watchtower:latest
|
|
container_name: towerops-watchtower
|
|
restart: unless-stopped
|
|
environment:
|
|
# Check for updates every 15 minutes (900 seconds)
|
|
- WATCHTOWER_POLL_INTERVAL=900
|
|
- WATCHTOWER_LABEL_ENABLE=true
|
|
- WATCHTOWER_CLEANUP=true
|
|
- WATCHTOWER_LOG_LEVEL=info
|
|
# Use latest Docker API version
|
|
- DOCKER_API_VERSION=1.44
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock</code></pre>
|
|
<button
|
|
type="button"
|
|
phx-hook="CopyToClipboard"
|
|
data-target="#docker-compose-example"
|
|
id={"copy-docker-compose-#{@new_token.id}"}
|
|
class="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
|
|
>
|
|
<.icon name="hero-clipboard" class="h-3 w-3" /> Copy to clipboard
|
|
</button>
|
|
<textarea id="docker-compose-example" class="sr-only" readonly>services:
|
|
towerops-agent:
|
|
image: {@agent_image}
|
|
container_name: towerops-agent
|
|
restart: unless-stopped
|
|
environment:
|
|
- TOWEROPS_API_URL={url(@socket, ~p"/")}
|
|
- TOWEROPS_AGENT_TOKEN={@new_token.token}
|
|
volumes:
|
|
- ./data:/data
|
|
labels:
|
|
- "com.centurylinklabs.watchtower.enable=true"
|
|
- "com.centurylinklabs.watchtower.scope=towerops"
|
|
|
|
watchtower:
|
|
image: containrrr/watchtower:latest
|
|
container_name: towerops-watchtower
|
|
restart: unless-stopped
|
|
environment:
|
|
# Check for updates every 15 minutes (900 seconds)
|
|
- WATCHTOWER_POLL_INTERVAL=900
|
|
- WATCHTOWER_LABEL_ENABLE=true
|
|
- WATCHTOWER_CLEANUP=true
|
|
- WATCHTOWER_LOG_LEVEL=info
|
|
# Use latest Docker API version
|
|
- DOCKER_API_VERSION=1.44
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 flex justify-end">
|
|
<.button phx-click="close_token_modal" variant="primary">
|
|
I've Saved the Token
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</Layouts.authenticated>
|