The Docker agent now handles permissions automatically via the entrypoint script, so users don't need to manually create the data directory or run chown commands. Reverted the UI back to the simple docker-compose example.
256 lines
9.4 KiB
Text
256 lines
9.4 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">
|
|
<div class="font-medium text-zinc-900 dark:text-zinc-100">{agent.name}</div>
|
|
<%= 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 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
|
|
{label}
|
|
</span>
|
|
</:col>
|
|
|
|
<:col :let={agent} label="Equipment">
|
|
<div class="text-sm text-zinc-900 dark:text-zinc-100">
|
|
{Map.get(@equipment_counts, agent.id, 0)}
|
|
</div>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
|
directly assigned
|
|
</div>
|
|
</:col>
|
|
|
|
<:col :let={agent} label="Last Seen">
|
|
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
|
{format_last_seen(agent.last_seen_at)}
|
|
</div>
|
|
<%= if agent.last_ip do %>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-500 mt-0.5">
|
|
{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-click={
|
|
JS.dispatch("phx:copy", to: "#agent-token-input")
|
|
|> JS.push("token_copied")
|
|
}
|
|
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 Example
|
|
</h4>
|
|
<pre class="text-xs font-mono text-zinc-600 dark:text-zinc-400 overflow-x-auto"><code>services:
|
|
towerops-agent:
|
|
image: {@agent_image}
|
|
restart: unless-stopped
|
|
environment:
|
|
- TOWEROPS_API_URL={url(@socket, ~p"/")}
|
|
- TOWEROPS_AGENT_TOKEN={@new_token.token}
|
|
volumes:
|
|
- ./data:/data</code></pre>
|
|
<button
|
|
type="button"
|
|
phx-click={JS.dispatch("phx:copy", to: "#docker-compose-example")}
|
|
class="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
|
>
|
|
Copy to clipboard
|
|
</button>
|
|
<textarea id="docker-compose-example" class="sr-only">services:
|
|
towerops-agent:
|
|
image: {@agent_image}
|
|
restart: unless-stopped
|
|
environment:
|
|
- TOWEROPS_API_URL={url(@socket, ~p"/")}
|
|
- TOWEROPS_AGENT_TOKEN={@new_token.token}
|
|
volumes:
|
|
- ./data:/data</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>
|