Backend: - Migration: beacon_monitors table (binary_id PK, org/user FK, check_type, target_url, config, monitoring fields) - Schema: BeaconMonitor with changeset (pattern-matched port validation, check_type inclusion) - Query: BeaconMonitorQuery — composable scopes (by org, by user, by type, enabled, ordered, preloaded) - Context: Towerops.Beacons — CRUD, toggle, record_check_result (36/36 tests pass) Frontend: - User LiveView: /beacons — stream-based list, create/edit modals, toggle/delete actions - Admin LiveView: /admin/beacons — cross-org view with preloads (20/20 tests pass) - Form component: LiveComponent modal with validation - Helpers: check_type badges, status indicators, response time formatting - Router: user routes + admin route configured - Nav: sidebar + mobile links added - 30+ gettext translations Verification: compile ✓, format ✓, credo ✓, 56/56 new tests pass
150 lines
5.5 KiB
Text
150 lines
5.5 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
active_page="beacons"
|
|
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
|
|
>
|
|
<.header>
|
|
{@page_title}
|
|
<:subtitle>{t_equipment("Monitor your endpoints from distributed locations")}</:subtitle>
|
|
<:actions>
|
|
<.button phx-click="open_new" variant="primary">
|
|
<.icon name="hero-plus" class="h-5 w-5" /> {t("Add Beacon Monitor")}
|
|
</.button>
|
|
</:actions>
|
|
</.header>
|
|
|
|
<%= if !@has_beacons do %>
|
|
<div class="text-center py-16">
|
|
<.icon name="hero-signal" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
|
|
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t("No beacon monitors")}
|
|
</h3>
|
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
{t_equipment("Get started by creating your first beacon monitor.")}
|
|
</p>
|
|
<div class="mt-6">
|
|
<.button phx-click="open_new" variant="primary">
|
|
<.icon name="hero-plus" class="h-5 w-5" /> {t("Add Beacon Monitor")}
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<div class="mt-6 overflow-x-auto">
|
|
<.table
|
|
id="beacons-table"
|
|
rows={@streams.beacons}
|
|
>
|
|
<:col :let={{_id, beacon}} label={t_equipment("Name")}>
|
|
<span class="font-medium text-gray-900 dark:text-white">{beacon.name}</span>
|
|
</:col>
|
|
|
|
<:col :let={{_id, beacon}} label={t_equipment("Check Type")}>
|
|
<span class={[
|
|
"inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset",
|
|
check_type_badge_class(beacon.check_type)
|
|
]}>
|
|
{check_type_label(beacon.check_type)}
|
|
</span>
|
|
</:col>
|
|
|
|
<:col :let={{_id, beacon}} label={t_equipment("Target URL")}>
|
|
<span
|
|
class="text-sm text-gray-600 dark:text-gray-400 font-mono max-w-[200px] truncate block"
|
|
title={beacon.target_url}
|
|
>
|
|
{beacon.target_url}
|
|
</span>
|
|
<%= if beacon.check_type in ~w(tcp icmp) && beacon.target_port do %>
|
|
<span class="text-xs text-gray-500 dark:text-gray-500">:{beacon.target_port}</span>
|
|
<% end %>
|
|
</:col>
|
|
|
|
<:col :let={{_id, beacon}} label={t_equipment("Last Status")}>
|
|
<span class={[
|
|
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium",
|
|
status_badge_class(beacon.last_status)
|
|
]}>
|
|
<span class={["h-1.5 w-1.5 rounded-full", status_dot_class(beacon.last_status)]} />
|
|
{status_label(beacon.last_status)}
|
|
</span>
|
|
</:col>
|
|
|
|
<:col :let={{_id, beacon}} label={t_equipment("Response Time")}>
|
|
<%= if beacon.last_response_time_ms do %>
|
|
<span class="text-sm text-gray-600 dark:text-gray-400 font-mono">
|
|
{format_response_time(beacon.last_response_time_ms)}
|
|
</span>
|
|
<% else %>
|
|
<span class="text-sm text-gray-400 dark:text-gray-500">—</span>
|
|
<% end %>
|
|
</:col>
|
|
|
|
<:col :let={{_id, beacon}} label={t_equipment("Last Checked")}>
|
|
<%= if beacon.last_checked_at do %>
|
|
<.timestamp datetime={beacon.last_checked_at} timezone={@timezone} now={@now} />
|
|
<% else %>
|
|
<span class="text-sm text-gray-400 dark:text-gray-500">{t_equipment("Never")}</span>
|
|
<% end %>
|
|
</:col>
|
|
|
|
<:col :let={{_id, beacon}} label={t("Enabled")}>
|
|
<button
|
|
id={"toggle-beacon-#{beacon.id}"}
|
|
type="button"
|
|
phx-click="toggle_beacon"
|
|
phx-value-id={beacon.id}
|
|
class={[
|
|
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2",
|
|
if(beacon.enabled, do: "bg-blue-600", else: "bg-gray-300 dark:bg-gray-600")
|
|
]}
|
|
role="switch"
|
|
aria-checked={beacon.enabled}
|
|
>
|
|
<span class={[
|
|
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out",
|
|
if(beacon.enabled, do: "translate-x-5", else: "translate-x-0")
|
|
]} />
|
|
</button>
|
|
</:col>
|
|
|
|
<:action :let={{_id, beacon}}>
|
|
<div class="flex items-center gap-2">
|
|
<.button
|
|
id={"edit-beacon-#{beacon.id}"}
|
|
type="button"
|
|
phx-click="open_edit"
|
|
phx-value-id={beacon.id}
|
|
variant="secondary"
|
|
>
|
|
{t("Edit")}
|
|
</.button>
|
|
<.button
|
|
id={"delete-beacon-#{beacon.id}"}
|
|
type="button"
|
|
phx-click="delete_beacon"
|
|
phx-value-id={beacon.id}
|
|
data-confirm={t_equipment("Are you sure you want to delete this beacon monitor?")}
|
|
variant="danger"
|
|
>
|
|
{t("Delete")}
|
|
</.button>
|
|
</div>
|
|
</:action>
|
|
</.table>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Create/Edit Beacon Modal -->
|
|
<%= if @show_modal do %>
|
|
<.live_component
|
|
module={ToweropsWeb.BeaconMonitorLive.FormComponent}
|
|
id={
|
|
if @modal_action == :edit, do: "beacon-form-#{@modal_beacon.id}", else: "beacon-form-new"
|
|
}
|
|
action={@modal_action}
|
|
beacon={@modal_beacon}
|
|
current_scope={@current_scope}
|
|
/>
|
|
<% end %>
|
|
</Layouts.authenticated>
|