prop/lib/microwaveprop_web/live/admin/monitor_live/index.ex

226 lines
7.6 KiB
Elixir

defmodule MicrowavepropWeb.Admin.MonitorLive.Index do
@moduledoc "Admin beacon-monitor list at `/admin/beacon-monitors`."
use MicrowavepropWeb, :live_view
import Ecto.Query
alias Microwaveprop.Accounts.User
alias Microwaveprop.BeaconMonitors
alias Microwaveprop.BeaconMonitors.BeaconMonitor
alias Microwaveprop.Repo
@impl true
def mount(_params, _session, socket) do
users = Repo.all(from u in User, order_by: u.callsign)
{:ok,
socket
|> assign(:page_title, "Beacon monitors")
|> assign(:users, users)
|> assign(:form, to_form(BeaconMonitors.change_monitor()))
|> stream(:monitors, [])}
end
@impl true
def handle_params(_params, _url, socket) do
monitors = BeaconMonitors.list_all_monitors()
{:noreply, stream(socket, :monitors, monitors, reset: true)}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope} max_width="max-w-6xl">
<.header>
Beacon monitors
<:subtitle>Provision and manage physical monitor hardware.</:subtitle>
</.header>
<div class="flex justify-end mb-4">
<.button phx-click={JS.push("show-create-form", target: @myself)} variant="primary">
<.icon name="hero-plus" class="size-4" /> New monitor
</.button>
</div>
<div
:if={@form.source.action in [:insert, nil]}
id="create-form"
class="card bg-base-200 shadow-sm mb-6"
>
<div class="card-body">
<h3 class="card-title text-sm">Register new hardware</h3>
<.form for={@form} id="monitor-form" phx-submit="create" phx-change="validate">
<div class="grid grid-cols-2 gap-3">
<.input field={@form[:name]} type="text" label="Name" required />
<.input
field={@form[:user_id]}
type="select"
label="Assign to"
options={user_options(@users)}
/>
<.input
field={@form[:hardware_type]}
type="text"
label="Hardware type"
placeholder="RTL-SDR"
/>
<.input
field={@form[:hardware_id]}
type="text"
label="Hardware ID / Serial"
placeholder="SN-001"
/>
<.input field={@form[:firmware_version]} type="text" label="Firmware version" />
<.input
field={@form[:antenna_type]}
type="text"
label="Antenna type"
placeholder="Dipole"
/>
<.input
field={@form[:antenna_gain_dbi]}
type="number"
step="any"
label="Antenna gain (dBi)"
/>
</div>
<div class="grid grid-cols-2 gap-3 mt-3">
<.input field={@form[:lat]} type="number" step="any" label="Latitude" />
<.input field={@form[:lon]} type="number" step="any" label="Longitude" />
</div>
<footer class="mt-4 flex gap-2">
<.button variant="primary" phx-disable-with="Creating...">Create</.button>
<.button phx-click={JS.push("hide-create-form", target: @myself)}>Cancel</.button>
</footer>
</.form>
</div>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra table-sm">
<thead>
<tr>
<th>Name</th>
<th>Hardware</th>
<th>Assigned to</th>
<th>Monitoring</th>
<th>Last seen</th>
<th class="w-1"></th>
</tr>
</thead>
<tbody>
<tr :for={{id, monitor} <- @streams.monitors}>
<td class="font-semibold">{monitor.name}</td>
<td class="text-sm">
<%= if monitor.hardware_type do %>
{monitor.hardware_type}
<span :if={monitor.hardware_id} class="opacity-60 text-xs"> ({monitor.hardware_id})</span>
<% else %>
<span class="opacity-50">&mdash;</span>
<% end %>
</td>
<td class="text-sm">
<%= if monitor.user do %>
<.link navigate={~p"/u/#{monitor.user.callsign}"} class="link link-hover font-mono">
{monitor.user.callsign}
</.link>
<% else %>
<span class="opacity-50">Unassigned</span>
<% end %>
</td>
<td class="text-sm">
<%= if monitor.beacon do %>
<.link
navigate={~p"/beacons/#{monitor.beacon.id}"}
class="link link-hover font-mono"
>
{monitor.beacon.callsign}
</.link>
<% else %>
<span class="opacity-50">&mdash;</span>
<% end %>
</td>
<td class="text-sm opacity-70">
<%= if monitor.last_seen_at do %>
{Calendar.strftime(monitor.last_seen_at, "%Y-%m-%d %H:%M UTC")}
<% else %>
never
<% end %>
</td>
<td>
<div class="flex gap-1">
<.link
navigate={~p"/admin/beacon-monitors/#{monitor.id}"}
class="btn btn-xs btn-ghost"
>
Detail
</.link>
<.link
phx-click={JS.push("delete", value: %{id: monitor.id}, target: @myself)}
data-confirm={"Delete monitor '#{monitor.name}'? This cannot be undone."}
class="btn btn-xs btn-ghost text-error"
>
Delete
</.link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</Layouts.app>
"""
end
@impl true
def handle_event("show-create-form", _params, socket) do
form = to_form(BeaconMonitors.change_monitor(), as: :beacon_monitor)
{:noreply, assign(socket, :form, form)}
end
def handle_event("hide-create-form", _params, socket) do
{:noreply, assign(socket, :form, to_form(BeaconMonitors.change_monitor(), action: :ignore))}
end
def handle_event("validate", %{"beacon_monitor" => params}, socket) do
changeset =
%BeaconMonitor{}
|> BeaconMonitor.provision_changeset(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :form, to_form(changeset, as: :beacon_monitor))}
end
def handle_event("create", %{"beacon_monitor" => params}, socket) do
admin = socket.assigns.current_scope.user
case BeaconMonitors.create_hardware(admin, params) do
{:ok, monitor} ->
{:noreply,
socket
|> put_flash(:info, "Monitor '#{monitor.name}' created.")
|> assign(:form, to_form(BeaconMonitors.change_monitor(), action: :ignore))
|> stream(:monitors, [monitor], at: 0)}
{:error, changeset} ->
{:noreply, assign(socket, :form, to_form(changeset, as: :beacon_monitor))}
end
end
def handle_event("delete", %{"id" => id}, socket) do
case BeaconMonitors.delete_monitor!(id) do
{:ok, monitor} ->
{:noreply,
socket
|> put_flash(:info, "Monitor '#{monitor.name}' deleted.")
|> stream_delete(:monitors, monitor)}
{:error, :not_found} ->
{:noreply, put_flash(socket, :error, "Monitor not found.")}
end
end
defp user_options(users) do
Enum.map(users, &{&1.callsign, &1.id})
end
end