Convert /beacons approved list to live_table
Replaces the stream-backed static table for approved beacons with LiveTable.LiveResource, giving users column sorting and full-text search across callsign and grid. Custom renderers preserve the frequency/EIRP formatting, keying label, and on-air badge so the visual output matches the previous daisyUI cells. A new Beacons.approved_beacons_query/0 returns the base Ecto query (`where: approved == true`), assigned onto socket.assigns.data_provider in mount so live_table's handle_params uses it as the root query and applies sort/search/paginate on top. The leaflet map above the table keeps its existing phx-hook+phx-update=ignore setup, and the pending- approval admin section stays as a stream-backed `<.table>` — live_table is single-resource-per-liveview and the pending list is a distinct admin concern anyway. PubSub updates (beacon created/updated/deleted) now push_patch to the current path so live_table re-runs handle_params and the table refreshes in place.
This commit is contained in:
parent
67d64109e6
commit
f9264908db
2 changed files with 162 additions and 132 deletions
|
|
@ -39,6 +39,15 @@ defmodule Microwaveprop.Beacons do
|
|||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Base Ecto query for approved beacons. Used as a `live_table`
|
||||
`data_provider` so sort/search/pagination can be applied on top.
|
||||
"""
|
||||
@spec approved_beacons_query() :: Ecto.Query.t()
|
||||
def approved_beacons_query do
|
||||
from b in Beacon, as: :resource, where: b.approved == true
|
||||
end
|
||||
|
||||
@doc "Returns unapproved beacons awaiting admin review."
|
||||
@spec list_pending_beacons() :: [Beacon.t()]
|
||||
def list_pending_beacons do
|
||||
|
|
|
|||
|
|
@ -2,9 +2,155 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
|||
@moduledoc false
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
use LiveTable.LiveResource, schema: Microwaveprop.Beacons.Beacon
|
||||
|
||||
alias Microwaveprop.Beacons
|
||||
alias Microwaveprop.Beacons.Beacon
|
||||
|
||||
def fields do
|
||||
[
|
||||
id: %{label: "ID", hidden: true},
|
||||
frequency_mhz: %{
|
||||
label: "Frequency (MHz)",
|
||||
sortable: true,
|
||||
searchable: false,
|
||||
renderer: &freq_cell/1
|
||||
},
|
||||
callsign: %{label: "Call", sortable: true, searchable: true},
|
||||
grid: %{label: "Grid", sortable: true, searchable: true},
|
||||
power_mw: %{label: "EIRP (mW)", sortable: true, renderer: &mw_cell/1},
|
||||
height_ft: %{label: "Height AGL (ft)", sortable: true},
|
||||
keying: %{label: "Keying", sortable: true, renderer: &keying_cell/1},
|
||||
on_the_air: %{label: "On air", sortable: true, renderer: &on_air_cell/1}
|
||||
]
|
||||
end
|
||||
|
||||
def filters, do: []
|
||||
|
||||
def actions do
|
||||
[
|
||||
show: fn %{record: beacon} ->
|
||||
assigns = %{beacon: beacon}
|
||||
|
||||
~H"""
|
||||
<.link navigate={~p"/beacons/#{@beacon.id}"} class="btn btn-xs btn-ghost">Details</.link>
|
||||
"""
|
||||
end
|
||||
]
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket), do: Beacons.subscribe_beacons()
|
||||
|
||||
beacons = Beacons.list_beacons()
|
||||
|
||||
pending =
|
||||
if admin?(socket.assigns.current_scope) do
|
||||
Beacons.list_pending_beacons()
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Beacons")
|
||||
|> assign(:pending, pending)
|
||||
|> assign(:beacons_json, encode_beacons(beacons))
|
||||
|> assign(:data_provider, {Microwaveprop.Beacons, :approved_beacons_query, []})
|
||||
|> stream(:pending, pending)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
if admin?(socket.assigns.current_scope) do
|
||||
beacon = Beacons.get_beacon!(id)
|
||||
{:ok, _} = Beacons.delete_beacon(beacon)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> stream_delete(:pending, beacon)
|
||||
|> push_patch(to: socket.assigns.current_path |> path_with_prefix())}
|
||||
else
|
||||
{:noreply, put_flash(socket, :error, "Admins only.")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("approve", %{"id" => id}, socket) do
|
||||
if admin?(socket.assigns.current_scope) do
|
||||
beacon = Beacons.get_beacon!(id)
|
||||
{:ok, approved} = Beacons.approve_beacon(beacon)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Approved #{approved.callsign}.")
|
||||
|> stream_delete(:pending, beacon)
|
||||
|> push_patch(to: socket.assigns.current_path |> path_with_prefix())}
|
||||
else
|
||||
{:noreply, put_flash(socket, :error, "Admins only.")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({type, %Beacon{}}, socket) when type in [:created, :updated, :deleted] do
|
||||
beacons = Beacons.list_beacons()
|
||||
|
||||
pending =
|
||||
if admin?(socket.assigns.current_scope) do
|
||||
Beacons.list_pending_beacons()
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:pending, pending)
|
||||
|> assign(:beacons_json, encode_beacons(beacons))
|
||||
|> stream(:pending, pending, reset: true)
|
||||
|> push_patch(to: socket.assigns.current_path |> path_with_prefix())}
|
||||
end
|
||||
|
||||
defp admin?(%{user: %{is_admin: true}}), do: true
|
||||
defp admin?(_), do: false
|
||||
|
||||
defp path_with_prefix(path) when is_binary(path), do: "/" <> path
|
||||
defp path_with_prefix(_), do: "/beacons"
|
||||
|
||||
defp encode_beacons(beacons) do
|
||||
beacons
|
||||
|> Enum.filter(&(is_number(&1.lat) and is_number(&1.lon)))
|
||||
|> Enum.map(fn b ->
|
||||
%{
|
||||
id: b.id,
|
||||
callsign: b.callsign,
|
||||
lat: b.lat,
|
||||
lon: b.lon,
|
||||
frequency_mhz: b.frequency_mhz,
|
||||
grid: b.grid,
|
||||
keying: Beacon.keying_label(b.keying),
|
||||
on_the_air: b.on_the_air
|
||||
}
|
||||
end)
|
||||
|> Jason.encode!()
|
||||
end
|
||||
|
||||
defp freq_cell(freq), do: Beacon.format_freq(freq)
|
||||
defp mw_cell(power), do: Beacon.format_mw(power)
|
||||
defp keying_cell(keying), do: Beacon.keying_label(keying)
|
||||
|
||||
defp on_air_cell(on_the_air) do
|
||||
assigns = %{on_the_air: on_the_air}
|
||||
|
||||
~H"""
|
||||
<span class={[
|
||||
"badge badge-sm",
|
||||
(@on_the_air && "badge-success") || "badge-ghost"
|
||||
]}>
|
||||
{if @on_the_air, do: "Yes", else: "No"}
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
|
@ -28,40 +174,13 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
|||
>
|
||||
</div>
|
||||
|
||||
<.table
|
||||
id="beacons"
|
||||
rows={@streams.beacons}
|
||||
row_click={fn {_id, beacon} -> JS.navigate(~p"/beacons/#{beacon}") end}
|
||||
>
|
||||
<:col :let={{_id, beacon}} label="Frequency (MHz)">
|
||||
{Beacon.format_freq(beacon.frequency_mhz)}
|
||||
</:col>
|
||||
<:col :let={{_id, beacon}} label="Call">{beacon.callsign}</:col>
|
||||
<:col :let={{_id, beacon}} label="Grid">{beacon.grid}</:col>
|
||||
<:col :let={{_id, beacon}} label="EIRP (mW)">{Beacon.format_mw(beacon.power_mw)}</:col>
|
||||
<:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft}</:col>
|
||||
<:col :let={{_id, beacon}} label="Keying">{Beacon.keying_label(beacon.keying)}</:col>
|
||||
<:col :let={{_id, beacon}} label="On air">
|
||||
<span class={["badge badge-sm", (beacon.on_the_air && "badge-success") || "badge-ghost"]}>
|
||||
{if beacon.on_the_air, do: "Yes", else: "No"}
|
||||
</span>
|
||||
</:col>
|
||||
<:action :let={{_id, beacon}}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/beacons/#{beacon}"}>Show</.link>
|
||||
</div>
|
||||
<.link :if={admin?(@current_scope)} navigate={~p"/beacons/#{beacon}/edit"}>Edit</.link>
|
||||
</:action>
|
||||
<:action :let={{id, beacon}}>
|
||||
<.link
|
||||
:if={admin?(@current_scope)}
|
||||
phx-click={JS.push("delete", value: %{id: beacon.id}) |> hide("##{id}")}
|
||||
data-confirm="Delete this beacon?"
|
||||
>
|
||||
Delete
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
<.live_table
|
||||
fields={fields()}
|
||||
filters={filters()}
|
||||
options={@options}
|
||||
streams={@streams}
|
||||
actions={actions()}
|
||||
/>
|
||||
|
||||
<div :if={admin?(@current_scope) and @pending != []} class="mt-10">
|
||||
<.header>
|
||||
|
|
@ -105,102 +224,4 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
|||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket), do: Beacons.subscribe_beacons()
|
||||
|
||||
beacons = Beacons.list_beacons()
|
||||
|
||||
pending =
|
||||
if admin?(socket.assigns.current_scope) do
|
||||
Beacons.list_pending_beacons()
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Beacons")
|
||||
|> assign(:pending, pending)
|
||||
|> assign(:beacons_json, encode_beacons(beacons))
|
||||
|> stream(:beacons, beacons)
|
||||
|> stream(:pending, pending)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
if admin?(socket.assigns.current_scope) do
|
||||
beacon = Beacons.get_beacon!(id)
|
||||
{:ok, _} = Beacons.delete_beacon(beacon)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> stream_delete(:beacons, beacon)
|
||||
|> stream_delete(:pending, beacon)}
|
||||
else
|
||||
{:noreply, put_flash(socket, :error, "Admins only.")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("approve", %{"id" => id}, socket) do
|
||||
if admin?(socket.assigns.current_scope) do
|
||||
beacon = Beacons.get_beacon!(id)
|
||||
{:ok, approved} = Beacons.approve_beacon(beacon)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Approved #{approved.callsign}.")
|
||||
|> stream_delete(:pending, beacon)
|
||||
|> stream_insert(:beacons, approved)}
|
||||
else
|
||||
{:noreply, put_flash(socket, :error, "Admins only.")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({type, %Beacon{}}, socket) when type in [:created, :updated, :deleted] do
|
||||
beacons = Beacons.list_beacons()
|
||||
|
||||
pending =
|
||||
if admin?(socket.assigns.current_scope) do
|
||||
Beacons.list_pending_beacons()
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:pending, pending)
|
||||
|> assign(:beacons_json, encode_beacons(beacons))
|
||||
|> stream(:beacons, beacons, reset: true)
|
||||
|> stream(:pending, pending, reset: true)}
|
||||
end
|
||||
|
||||
defp admin?(%{user: %{is_admin: true}}), do: true
|
||||
defp admin?(_), do: false
|
||||
|
||||
# Serialize just the fields the map hook needs. `phx-update="ignore"`
|
||||
# means this data attribute is only read on initial hook mount — the
|
||||
# map won't re-render when beacons change without a full LV reload.
|
||||
# For now that's fine because list churn is rare; upgrade to
|
||||
# push_event("update_beacons", ...) if we start getting live PubSub
|
||||
# updates that need to reflect on the map without a refresh.
|
||||
defp encode_beacons(beacons) do
|
||||
beacons
|
||||
|> Enum.filter(&(is_number(&1.lat) and is_number(&1.lon)))
|
||||
|> Enum.map(fn b ->
|
||||
%{
|
||||
id: b.id,
|
||||
callsign: b.callsign,
|
||||
lat: b.lat,
|
||||
lon: b.lon,
|
||||
frequency_mhz: b.frequency_mhz,
|
||||
grid: b.grid,
|
||||
keying: Beacon.keying_label(b.keying),
|
||||
on_the_air: b.on_the_air
|
||||
}
|
||||
end)
|
||||
|> Jason.encode!()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue