246 lines
9.1 KiB
Text
246 lines
9.1 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="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: gmcintire/towerops-agent:latest
|
|
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: gmcintire/towerops-agent:latest
|
|
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>
|