feat(server): vm search LiveView with name+IP filtering
This commit is contained in:
parent
d65832964e
commit
94034eea9b
2 changed files with 175 additions and 0 deletions
104
server/lib/server_web/live/vm_search_live.ex
Normal file
104
server/lib/server_web/live/vm_search_live.ex
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
defmodule ServerWeb.VmSearchLive do
|
||||||
|
use ServerWeb, :live_view
|
||||||
|
|
||||||
|
alias Server.{Hosts, Metrics}
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
if connected?(socket), do: Phoenix.PubSub.subscribe(Server.PubSub, "metrics")
|
||||||
|
{:ok, socket |> assign(:q, "") |> assign(:vms, load_vms())}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:metric_inserted, _, _}, socket) do
|
||||||
|
{:noreply, assign(socket, :vms, load_vms())}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("search", %{"q" => q}, socket) do
|
||||||
|
{:noreply, assign(socket, :q, q)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_vms do
|
||||||
|
for host <- Hosts.list_all(),
|
||||||
|
runtime_sample = Metrics.latest_sample(host.id, "fast"),
|
||||||
|
vm <- get_in(runtime_sample && runtime_sample.payload, ["vms_runtime", "vms"]) || [],
|
||||||
|
into: [] do
|
||||||
|
detail_sample = Metrics.latest_sample(host.id, "medium")
|
||||||
|
detail_vms = get_in(detail_sample && detail_sample.payload, ["vms_detail", "vms"]) || []
|
||||||
|
detail_vm = Enum.find(detail_vms, &(&1["vmid"] == vm["vmid"])) || %{}
|
||||||
|
ips = detail_vm["ips"] || []
|
||||||
|
|
||||||
|
%{
|
||||||
|
vmid: vm["vmid"],
|
||||||
|
name: vm["name"],
|
||||||
|
type: vm["type"],
|
||||||
|
status: vm["status"],
|
||||||
|
host_name: host.name,
|
||||||
|
ips: ips
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp filter(vms, ""), do: vms
|
||||||
|
|
||||||
|
defp filter(vms, q) do
|
||||||
|
q = String.downcase(q)
|
||||||
|
|
||||||
|
Enum.filter(vms, fn vm ->
|
||||||
|
String.contains?(String.downcase(vm.name || ""), q) or
|
||||||
|
Enum.any?(vm.ips, &String.contains?(&1, q))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="p-6 max-w-6xl mx-auto space-y-4">
|
||||||
|
<.link navigate={~p"/"} class="text-sm text-zinc-500 hover:text-zinc-900">← Back</.link>
|
||||||
|
<h1 class="text-2xl font-bold">VM Search</h1>
|
||||||
|
|
||||||
|
<form phx-change="search">
|
||||||
|
<input
|
||||||
|
name="q"
|
||||||
|
value={@q}
|
||||||
|
placeholder="Search by name or IP…"
|
||||||
|
autofocus
|
||||||
|
class="w-full rounded-md border-zinc-300 focus:border-zinc-400 focus:ring-0"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table class="w-full text-sm bg-white border rounded-lg">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left text-zinc-500 border-b">
|
||||||
|
<th class="py-2 px-3">Name</th>
|
||||||
|
<th class="py-2 px-3">Host</th>
|
||||||
|
<th class="py-2 px-3">Type</th>
|
||||||
|
<th class="py-2 px-3">Status</th>
|
||||||
|
<th class="py-2 px-3">IPs</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr :for={vm <- filter(@vms, @q)} class="border-b last:border-b-0">
|
||||||
|
<td class="py-2 px-3 font-mono">{vm.name}</td>
|
||||||
|
<td class="py-2 px-3">
|
||||||
|
<.link
|
||||||
|
navigate={~p"/hosts/#{vm.host_name}"}
|
||||||
|
class="text-zinc-700 hover:text-zinc-900 underline"
|
||||||
|
>
|
||||||
|
{vm.host_name}
|
||||||
|
</.link>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3">{vm.type}</td>
|
||||||
|
<td class="py-2 px-3">{vm.status}</td>
|
||||||
|
<td class="py-2 px-3 font-mono text-xs">{Enum.join(vm.ips, ", ")}</td>
|
||||||
|
</tr>
|
||||||
|
<tr :if={filter(@vms, @q) == []}>
|
||||||
|
<td colspan="5" class="py-4 px-3 text-center text-zinc-500">No matches.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
71
server/test/server_web/live/vm_search_live_test.exs
Normal file
71
server/test/server_web/live/vm_search_live_test.exs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
defmodule ServerWeb.VmSearchLiveTest do
|
||||||
|
use ServerWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
alias Server.{Hosts, Metrics}
|
||||||
|
|
||||||
|
defp auth(conn), do: Plug.Test.init_test_session(conn, %{authenticated: true})
|
||||||
|
|
||||||
|
setup do
|
||||||
|
{:ok, {h1, _}} = Hosts.create_host("pve-01")
|
||||||
|
{:ok, {h2, _}} = Hosts.create_host("pve-02")
|
||||||
|
|
||||||
|
fast1 = %{
|
||||||
|
"vms_runtime" => %{
|
||||||
|
"vms" => [
|
||||||
|
%{"vmid" => 100, "name" => "nginx-proxy", "type" => "qemu", "status" => "running"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fast2 = %{
|
||||||
|
"vms_runtime" => %{
|
||||||
|
"vms" => [
|
||||||
|
%{"vmid" => 200, "name" => "db-primary", "type" => "qemu", "status" => "running"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
medium1 = %{
|
||||||
|
"vms_detail" => %{
|
||||||
|
"vms" => [%{"vmid" => 100, "name" => "nginx-proxy", "ips" => ["192.168.1.10"]}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, _} = Metrics.record_sample(h1.id, "fast", DateTime.utc_now(), fast1)
|
||||||
|
{:ok, _} = Metrics.record_sample(h2.id, "fast", DateTime.utc_now(), fast2)
|
||||||
|
{:ok, _} = Metrics.record_sample(h1.id, "medium", DateTime.utc_now(), medium1)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "lists all VMs from all hosts by default", %{conn: conn} do
|
||||||
|
{:ok, _view, html} = live(auth(conn), "/vms")
|
||||||
|
assert html =~ "nginx-proxy"
|
||||||
|
assert html =~ "db-primary"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filters by name substring", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(auth(conn), "/vms")
|
||||||
|
|
||||||
|
html =
|
||||||
|
view
|
||||||
|
|> form("form", q: "nginx")
|
||||||
|
|> render_change()
|
||||||
|
|
||||||
|
assert html =~ "nginx-proxy"
|
||||||
|
refute html =~ "db-primary"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filters by IP substring (matches detail payload)", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(auth(conn), "/vms")
|
||||||
|
|
||||||
|
html =
|
||||||
|
view
|
||||||
|
|> form("form", q: "192.168.1")
|
||||||
|
|> render_change()
|
||||||
|
|
||||||
|
assert html =~ "nginx-proxy"
|
||||||
|
refute html =~ "db-primary"
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Add a link
Reference in a new issue