226 lines
7.6 KiB
Elixir
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">—</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">—</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
|