refactor(ui): minimalistic utilitarian redesign across all views
New design language:
- dark background, system sans for UI, monospace for data
- single green accent, amber/red for warn/critical
- square-bordered panels + tables, no rounded cards or shadows
- status conveyed via left-border on overview cards + badges
Changes:
- new app.css defines CSS vars + component classes (.panel, .tbl,
.card, .btn, .input, .badge with [data-status=*])
- new ServerWeb.DashboardNav function component for a shared top nav
with active-link highlighting; replaces per-view navigation clutter
- strip the Phoenix welcome scaffold (logo, version badge, twitter/GH
links) from layouts/app.html.heex; leaves only flash + content
- root.html.heex title suffix switched to 'Proxmox Monitor', body
loses the Tailwind-white background
- rewrite render/1 in all four LiveViews + login template to use the
new classes; admin form now uses <.form for={@form}> and properly
clears on success
- login page redesigned to a single tight panel matching the rest
All 58 tests still pass; 'mix compile --warnings-as-errors' is clean.
This commit is contained in:
parent
1b031ecdc3
commit
50676a7cb8
9 changed files with 649 additions and 364 deletions
|
|
@ -7,9 +7,13 @@ defmodule ServerWeb.AdminHostsLive do
|
|||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:hosts, Hosts.list_all())
|
||||
|> assign(:new_token, nil)
|
||||
|> assign(:error, nil)}
|
||||
|> assign(
|
||||
hosts: Hosts.list_all(),
|
||||
form: to_form(%{"name" => ""}, as: :host),
|
||||
new_token: nil,
|
||||
error: nil,
|
||||
page_title: "Admin"
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -18,9 +22,12 @@ defmodule ServerWeb.AdminHostsLive do
|
|||
{:ok, {host, token}} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:hosts, Hosts.list_all())
|
||||
|> assign(:new_token, %{name: host.name, token: token})
|
||||
|> assign(:error, nil)}
|
||||
|> assign(
|
||||
hosts: Hosts.list_all(),
|
||||
form: to_form(%{"name" => ""}, as: :host),
|
||||
new_token: %{name: host.name, token: token},
|
||||
error: nil
|
||||
)}
|
||||
|
||||
{:error, cs} ->
|
||||
{:noreply, assign(socket, :error, changeset_message(cs))}
|
||||
|
|
@ -33,8 +40,7 @@ defmodule ServerWeb.AdminHostsLive do
|
|||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:hosts, Hosts.list_all())
|
||||
|> assign(:new_token, %{name: host.name, token: token})}
|
||||
|> assign(hosts: Hosts.list_all(), new_token: %{name: host.name, token: token})}
|
||||
end
|
||||
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
|
|
@ -44,89 +50,93 @@ defmodule ServerWeb.AdminHostsLive do
|
|||
end
|
||||
|
||||
defp changeset_message(cs) do
|
||||
cs.errors
|
||||
|> Enum.map_join(", ", fn {k, {msg, _}} -> "#{k}: #{msg}" end)
|
||||
cs.errors |> Enum.map_join(", ", fn {k, {msg, _}} -> "#{k}: #{msg}" end)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl mx-auto space-y-6">
|
||||
<.link navigate={~p"/"} class="text-sm text-zinc-500 hover:text-zinc-900">← Back</.link>
|
||||
<h1 class="text-2xl font-bold">Hosts</h1>
|
||||
<ServerWeb.DashboardNav.nav active={:admin} />
|
||||
|
||||
<section class="bg-white border rounded-lg p-4">
|
||||
<h2 class="font-semibold mb-2">Register a new host</h2>
|
||||
<form phx-submit="create" class="flex gap-2">
|
||||
<input
|
||||
name="host[name]"
|
||||
placeholder="pve-hostname"
|
||||
required
|
||||
class="flex-1 rounded-md border-zinc-300 focus:border-zinc-400 focus:ring-0"
|
||||
/>
|
||||
<button type="submit" class="rounded-md bg-zinc-900 text-white px-4">Add</button>
|
||||
</form>
|
||||
<p :if={@error} class="text-sm text-red-600 mt-2">{@error}</p>
|
||||
<div class="page">
|
||||
<div class="pagehead">
|
||||
<h1>Admin · Hosts</h1>
|
||||
<span class="sub">{length(@hosts)} registered</span>
|
||||
</div>
|
||||
|
||||
<div :if={@new_token} class="mt-4 p-3 bg-amber-50 border border-amber-200 rounded">
|
||||
<p class="text-sm font-semibold text-amber-900">
|
||||
Token for {@new_token.name} (shown once):
|
||||
</p>
|
||||
<code class="block mt-1 break-all text-sm">{@new_token.token}</code>
|
||||
<div class="panel">
|
||||
<header><span>Register a host</span></header>
|
||||
<div class="body">
|
||||
<.form for={@form} phx-submit="create" class="form-row">
|
||||
<input
|
||||
name="host[name]"
|
||||
value=""
|
||||
placeholder="pve-hostname"
|
||||
required
|
||||
autocomplete="off"
|
||||
class="input"
|
||||
/>
|
||||
<button type="submit" class="btn primary">Add</button>
|
||||
</.form>
|
||||
<p :if={@error} class="callout err" style="margin-top: 0.7rem;">{@error}</p>
|
||||
<div :if={@new_token} class="callout ok" style="margin-top: 0.7rem;">
|
||||
Token for <strong class="mono">{@new_token.name}</strong> — shown once:<br/>
|
||||
<code class="mono" style="color: var(--fg-bright); user-select: all;">{@new_token.token}</code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="bg-white border rounded-lg">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-zinc-500 border-b">
|
||||
<th class="py-2 px-3">Name</th>
|
||||
<th class="py-2 px-3">Status</th>
|
||||
<th class="py-2 px-3">Agent</th>
|
||||
<th class="py-2 px-3">Last seen</th>
|
||||
<th class="py-2 px-3 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr :for={h <- @hosts} class="border-b last:border-b-0">
|
||||
<td class="py-2 px-3 font-mono">{h.name}</td>
|
||||
<td class="py-2 px-3">{h.status}</td>
|
||||
<td class="py-2 px-3">{h.agent_version || "—"}</td>
|
||||
<td class="py-2 px-3">{format_seen(h.last_seen_at)}</td>
|
||||
<td class="py-2 px-3 text-right space-x-2">
|
||||
<button
|
||||
phx-click="rotate"
|
||||
phx-value-id={h.id}
|
||||
class="text-xs text-zinc-700 underline"
|
||||
data-confirm={"Rotate token for #{h.name}? Old token will stop working."}
|
||||
>
|
||||
Rotate
|
||||
</button>
|
||||
<button
|
||||
phx-click="delete"
|
||||
phx-value-id={h.id}
|
||||
class="text-xs text-red-600 underline"
|
||||
data-confirm={"Delete #{h.name} and all its metrics?"}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr :if={@hosts == []}>
|
||||
<td colspan="5" class="py-4 px-3 text-center text-zinc-500">
|
||||
No hosts yet.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<div class="panel">
|
||||
<header><span>All hosts</span></header>
|
||||
<div class="body tight">
|
||||
<table class="tbl" :if={@hosts != []}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Agent</th>
|
||||
<th>Last seen</th>
|
||||
<th class="right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr :for={h <- @hosts}>
|
||||
<td class="mono">
|
||||
<.link navigate={~p"/hosts/#{h.name}"}>{h.name}</.link>
|
||||
</td>
|
||||
<td><span class="badge" data-status={h.status}>{h.status}</span></td>
|
||||
<td class="mono">{h.agent_version || "—"}</td>
|
||||
<td class="mono">{format_seen(h.last_seen_at)}</td>
|
||||
<td class="right">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="rotate"
|
||||
phx-value-id={h.id}
|
||||
class="btn sm ghost"
|
||||
data-confirm={"Rotate token for #{h.name}? Old token stops working immediately."}
|
||||
>
|
||||
rotate
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="delete"
|
||||
phx-value-id={h.id}
|
||||
class="btn sm danger"
|
||||
data-confirm={"Delete #{h.name} and all its metrics?"}
|
||||
>
|
||||
delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div :if={@hosts == []} class="empty">No hosts yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp format_seen(nil), do: "never"
|
||||
|
||||
defp format_seen(%DateTime{} = dt) do
|
||||
Calendar.strftime(dt, "%Y-%m-%d %H:%M UTC")
|
||||
end
|
||||
defp format_seen(%DateTime{} = dt), do: Calendar.strftime(dt, "%Y-%m-%d %H:%M UTC")
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue