- Add jump_credo_checks ~> 0.4 with all 20 checks enabled - Fix all standard Credo issues: 139 @spec (113 done, 26 remain), 4 refactoring, 3 alias usage, 9 System.cmd env, 5 unsafe_to_atom, 2 max line length, 9 assert_receive timeout - Fix 170+ jump_credo_checks warnings: - 117 TopLevelAliasImportRequire: move nested alias/import to module top - 32 UseObanProWorker: switch to Oban.Pro.Worker - 4 DoctestIExExamples: add doctests / create test file - ~20 WeakAssertion: strengthen type-check assertions - Various ConditionalAssertion, AssertReceiveTimeout fixes - Exclude vendor/ from Credo analysis - Remaining: 175 warnings (mostly opinionated WeakAssertion, AvoidSocketAssignsInTest), 26 @spec annotations
234 lines
6.7 KiB
Elixir
234 lines
6.7 KiB
Elixir
defmodule MicrowavepropWeb.BeaconLive.Index do
|
|
@moduledoc "`/beacons` list + admin controls for the beacon catalog."
|
|
use MicrowavepropWeb, :live_view
|
|
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Beacons.Beacon
|
|
|
|
alias Microwaveprop.Beacons
|
|
alias Microwaveprop.Beacons.Beacon
|
|
|
|
@spec table_options() :: map()
|
|
def table_options do
|
|
%{exports: %{formats: [:csv]}}
|
|
end
|
|
|
|
@spec fields() :: keyword()
|
|
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
|
|
|
|
@spec filters() :: list()
|
|
def filters, do: []
|
|
|
|
@spec actions() :: keyword()
|
|
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, {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: path_with_prefix(socket.assigns.current_path))}
|
|
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: path_with_prefix(socket.assigns.current_path))}
|
|
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: path_with_prefix(socket.assigns.current_path))}
|
|
end
|
|
|
|
defp admin?(%{user: %{is_admin: true}}), do: true
|
|
defp admin?(_), do: false
|
|
|
|
defp path_with_prefix(path) when is_binary(path), do: "/" <> String.trim_leading(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"""
|
|
<Layouts.app flash={@flash} current_scope={@current_scope} max_width="max-w-6xl">
|
|
<.header>
|
|
Beacons
|
|
<:subtitle>Microwave beacons tracked by NTMS.</:subtitle>
|
|
<:actions>
|
|
<.button variant="primary" navigate={~p"/beacons/new"}>
|
|
<.icon name="hero-plus" /> Submit Beacon
|
|
</.button>
|
|
</:actions>
|
|
</.header>
|
|
|
|
<div
|
|
id="beacons-map"
|
|
phx-hook="BeaconsListMap"
|
|
phx-update="ignore"
|
|
data-beacons={@beacons_json}
|
|
class="h-80 w-full rounded-lg border border-base-300 mb-6 z-0"
|
|
>
|
|
</div>
|
|
|
|
<.live_table
|
|
fields={fields()}
|
|
filters={filters()}
|
|
options={@options}
|
|
streams={@streams}
|
|
actions={actions()}
|
|
/>
|
|
|
|
<div :if={admin?(@current_scope) and @pending != []} class="mt-10">
|
|
<.header>
|
|
Pending approval
|
|
<:subtitle>Submitted beacons waiting for admin review.</:subtitle>
|
|
</.header>
|
|
|
|
<.table
|
|
id="pending-beacons"
|
|
rows={@streams.pending}
|
|
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="Submitted">
|
|
{Calendar.strftime(beacon.inserted_at, "%Y-%m-%d %H:%M UTC")}
|
|
</:col>
|
|
<:action :let={{_id, beacon}}>
|
|
<.link
|
|
phx-click={JS.push("approve", value: %{id: beacon.id})}
|
|
data-confirm="Approve this beacon?"
|
|
>
|
|
Approve
|
|
</.link>
|
|
</:action>
|
|
<:action :let={{id, beacon}}>
|
|
<.link
|
|
phx-click={JS.push("delete", value: %{id: beacon.id}) |> hide("##{id}")}
|
|
data-confirm="Delete this beacon?"
|
|
>
|
|
Delete
|
|
</.link>
|
|
</:action>
|
|
</.table>
|
|
</div>
|
|
</Layouts.app>
|
|
"""
|
|
end
|
|
end
|