towerops/lib/towerops_web/live/device_live/form.html.heex

779 lines
34 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
>
<div phx-hook="ScrollToTop" id="scroll-container">
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
<div class="mb-4">
<.link
navigate={
if @live_action == :edit,
do: ~p"/devices/#{@device.id}",
else: ~p"/devices"
}
class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
<.icon name="hero-arrow-left" class="h-4 w-4" />
{if @live_action == :edit, do: "Back to Device", else: "Back to Device List"}
</.link>
</div>
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
{@page_title}
</h1>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{if @live_action == :new,
do: "Add new device to monitor",
else: "Update device details"}
</p>
</div>
<%= if @live_action == :new do %>
<% percent =
if @device_quota.limit != :unlimited and @device_quota.limit > 0 do
(@device_quota.current / @device_quota.limit * 100) |> trunc()
else
0
end %>
<% badge_class =
cond do
@current_scope.user.is_superuser ->
"bg-blue-50 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
@device_quota.at_limit ->
"bg-red-50 text-red-800 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800"
percent >= 90 ->
"bg-orange-50 text-orange-800 border-orange-200 dark:bg-orange-900/20 dark:text-orange-400 dark:border-orange-800"
percent >= 75 ->
"bg-yellow-50 text-yellow-800 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800"
@device_quota.limit == :unlimited ->
"bg-blue-50 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
true ->
"bg-green-50 text-green-800 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800"
end %>
<div class={"rounded-lg px-3 py-2 text-sm font-medium border #{badge_class}"}>
<%= cond do %>
<% @current_scope.user.is_superuser -> %>
{@device_quota.current} devices
<% @device_quota.limit == :unlimited -> %>
{@device_quota.current} devices
<% true -> %>
{@device_quota.current}/{@device_quota.limit} devices
<% end %>
</div>
<% end %>
</div>
</div>
<%= if @live_action == :new and not @device_quota.at_limit and @device_quota.limit != :unlimited and not @current_scope.user.is_superuser do %>
<% remaining = @device_quota.limit - @device_quota.current %>
<%= if remaining <= 3 do %>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 mt-6">
<div class="rounded-lg bg-orange-50 dark:bg-orange-900/20 p-4 border border-orange-200 dark:border-orange-800">
<div class="flex">
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-orange-400" />
<div class="ml-3">
<h3 class="text-sm font-medium text-orange-800 dark:text-orange-400">
{t("Approaching device limit")}
</h3>
<p class="mt-1 text-sm text-orange-700 dark:text-orange-300">
You have {remaining} {if remaining == 1, do: "slot", else: "slots"} remaining.
</p>
</div>
</div>
</div>
</div>
<% end %>
<% end %>
<.form for={@form} id="device-form" phx-change="validate" phx-submit="save">
<div class="divide-y divide-gray-200 dark:divide-white/10">
<!-- Basic Information -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Basic Information")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Core device identification and location details.")}
</p>
</div>
<div class="md:col-span-2">
<div class="grid grid-cols-1 gap-x-6 gap-y-4 sm:max-w-xl sm:grid-cols-6">
<div class="col-span-full">
<.input
field={@form[:name]}
type="text"
label={t("Device Name")}
placeholder={
if @monitoring_mode == "snmp_and_icmp",
do: "Leave blank to automatically populate from SNMP device name (sysName)",
else: nil
}
required={@monitoring_mode == "icmp_only"}
/>
</div>
<div class="col-span-full">
<.input field={@form[:ip_address]} type="text" label={t("IP Address")} required />
<div
:if={@duplicate_device}
class="rounded-md bg-amber-50 dark:bg-amber-900/20 p-3 mt-2"
>
<div class="flex items-center gap-2 text-amber-800 dark:text-amber-200">
<.icon name="hero-exclamation-triangle" class="h-5 w-5 flex-shrink-0" />
<p class="text-sm">
{t("A device with this IP address already exists:")}
<.link
navigate={~p"/devices/#{@duplicate_device.id}"}
class="font-medium underline hover:text-amber-900 dark:hover:text-amber-100"
>
{@duplicate_device.name || @duplicate_device.ip_address}
</.link>
</p>
</div>
</div>
<div
:if={@non_routable_ip_error}
class="rounded-md bg-red-50 dark:bg-red-900/20 p-3 mt-2"
>
<div class="flex items-start gap-2 text-red-800 dark:text-red-200">
<.icon name="hero-exclamation-circle" class="h-5 w-5 flex-shrink-0 mt-0.5" />
<div class="text-sm">
<p class="font-medium">Non-routable IP address detected</p>
<p class="mt-1">
{t(
"This IP address (private network or CGNAT range) cannot be reached from the cloud."
)}
{t("To monitor this device, assign it to a")}
<.link navigate={~p"/agents"} class="font-medium underline">
remote agent
</.link>
deployed on the same network.
</p>
</div>
</div>
</div>
</div>
<div :if={@organization.use_sites} class="col-span-full">
<.input
field={@form[:site_id]}
type="select"
label={t("Site (Optional)")}
prompt="No site (ungrouped)"
options={Enum.map(@available_sites, &{&1.name, &1.id})}
/>
<p class="mt-1 text-sm text-gray-500">
{t("Optionally group this device into a site (office, datacenter, etc.)")}
</p>
</div>
<div class="col-span-full">
<.input field={@form[:description]} type="textarea" label={t("Description")} />
</div>
<div class="col-span-full">
<.input
field={@form[:device_role]}
type="select"
label={t("Device Role")}
prompt="Auto-detect"
options={[
{"Router", "router"},
{"Switch", "switch"},
{"Access Point", "access_point"},
{"CPE", "cpe"},
{"Firewall", "firewall"},
{"Server", "server"},
{"PDU", "pdu"},
{"UPS", "ups"},
{"Camera", "camera"},
{"Printer", "printer"}
]}
/>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("Select a role manually or leave as auto-detect to infer from SNMP data.")}
</p>
</div>
</div>
</div>
</div>
<!-- Monitoring Configuration -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Monitoring Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Configure how and how often this device should be monitored.")}
</p>
</div>
<div class="md:col-span-2">
<div class="grid grid-cols-1 gap-x-6 gap-y-4 sm:max-w-xl sm:grid-cols-6">
<div class="col-span-full">
<.input
field={@form[:check_interval_seconds]}
type="number"
label={t("Check Interval (seconds)")}
min="30"
max="3600"
/>
</div>
<div class="col-span-full">
<.input
field={@form[:monitoring_enabled]}
type="checkbox"
label={t("Enable Monitoring")}
/>
</div>
<%= if @available_agents != [] do %>
<div class="col-span-full">
<.input
field={@form[:agent_token_id]}
type="select"
label={t("Remote Agent")}
prompt="Inherit from site/organization"
options={
Enum.map(@available_agents, fn agent ->
label =
if agent.is_cloud_poller,
do: "#{agent.name} (Cloud Poller)",
else: agent.name
{label, agent.id}
end)
}
/>
<%= if @live_action == :edit and Map.has_key?(assigns, :agent_source) do %>
<%= case @agent_source do %>
<% :device -> %>
<p class="mt-2 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
<strong>{t("Overriding")}</strong>
site/organization defaults - this device is explicitly assigned
</p>
<% :site -> %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-building-office" class="h-4 w-4" />
Inherited from site: <strong>{@effective_agent_name}</strong>
</p>
<% :organization -> %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-building-office-2" class="h-4 w-4" />
Inherited from organization: <strong>{@effective_agent_name}</strong>
</p>
<% :global -> %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-globe-alt" class="h-4 w-4" />
{t("Using global default cloud poller:")}
<strong>{@effective_agent_name}</strong>
</p>
<% :none -> %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-cloud" class="h-4 w-4" />
{t("No agent assigned - cloud polling")}
</p>
<% end %>
<% else %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t(
"Assign this device to a remote agent for local SNMP polling. Leave empty to inherit from site or organization defaults."
)}
</p>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
<!-- SNMP Configuration -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("SNMP Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Choose monitoring mode and configure SNMP settings for detailed device information."
)}
</p>
</div>
<div class="md:col-span-2">
<div class="space-y-6">
<!-- Monitoring Mode Tabs -->
<div>
<div class="border-b border-gray-200 dark:border-white/10">
<nav class="-mb-px flex space-x-8" aria-label={t("Monitoring mode")}>
<button
type="button"
phx-click="switch_monitoring_mode"
phx-value-mode="snmp_and_icmp"
class={[
"whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium",
@monitoring_mode == "snmp_and_icmp" &&
"border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400",
@monitoring_mode != "snmp_and_icmp" &&
"border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-gray-300"
]}
>
{t("SNMP & ICMP")}
</button>
<button
type="button"
phx-click="switch_monitoring_mode"
phx-value-mode="icmp_only"
class={[
"whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium",
@monitoring_mode == "icmp_only" &&
"border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400",
@monitoring_mode != "icmp_only" &&
"border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-gray-300"
]}
>
{t("ICMP Only")}
</button>
</nav>
</div>
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
<%= if @monitoring_mode == "snmp_and_icmp" do %>
<p>
{t(
"Monitor device availability with ICMP pings and collect detailed information via SNMP"
)} (interfaces, sensors, system info).
</p>
<% else %>
<p>Monitor device availability with ICMP pings only (no SNMP discovery).</p>
<% end %>
</div>
</div>
<!-- SNMP Fields -->
<div
:if={@monitoring_mode == "snmp_and_icmp"}
class="grid grid-cols-1 gap-x-6 gap-y-4 sm:max-w-xl sm:grid-cols-6"
>
<div class="col-span-full">
<.input
field={@form[:snmp_version]}
type="select"
label={t("SNMP Version")}
prompt="Inherit from site/organization"
options={[{"v1", "1"}, {"v2c", "2c"}, {"v3", "3"}]}
/>
</div>
<!-- v1/v2c Community String -->
<div
:if={@form[:snmp_version].value in ["1", "2c"]}
class="col-span-full"
>
<.input
field={@form[:snmp_community]}
type="text"
label={t("Community String")}
placeholder={t("Leave blank to inherit from site/organization")}
autocomplete="off"
/>
<%= if @live_action == :edit and Map.has_key?(assigns, :snmp_config_source) do %>
<%= case @snmp_config_source do %>
<% :device -> %>
<p class="mt-2 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
<strong>{t("Overriding")}</strong> site/organization SNMP defaults
</p>
<% :site -> %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-building-office" class="h-4 w-4" />
Inherited from site (v{@effective_snmp_version}, community: {String.slice(
@effective_snmp_community,
0..1
)}{"*"
|> String.duplicate(
max(0, String.length(@effective_snmp_community) - 2)
)})
</p>
<% :organization -> %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-building-office-2" class="h-4 w-4" />
Inherited from organization (v{@effective_snmp_version}, community: {String.slice(
@effective_snmp_community,
0..1
)}{"*"
|> String.duplicate(
max(0, String.length(@effective_snmp_community) - 2)
)})
</p>
<% :default -> %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-information-circle" class="h-4 w-4" />
{t(
"Using default (v2c, no community set - configure at organization or site level)"
)}
</p>
<% end %>
<% end %>
</div>
<!-- v3 Credentials -->
<%= if @form[:snmp_version].value == "3" do %>
<div class="col-span-full">
<.input
field={@form[:snmpv3_username]}
type="text"
label={t("Username")}
placeholder={t("Leave blank to inherit from site/organization")}
/>
</div>
<% # Determine current security level from changeset (includes both data and changes)
changeset = @form.source
current_auth = Ecto.Changeset.get_field(changeset, :snmpv3_auth_protocol)
current_priv = Ecto.Changeset.get_field(changeset, :snmpv3_priv_protocol)
current_security_level =
cond do
current_auth in [nil, ""] -> "noAuthNoPriv"
current_priv not in [nil, ""] -> "authPriv"
true -> "authNoPriv"
end
# Get selected security level (from params if user is changing, otherwise current)
selected_security_level =
if Map.has_key?(@form.params, "snmpv3_security_level"),
do: @form.params["snmpv3_security_level"],
else: current_security_level %>
<!-- Auth Level Selector (ALWAYS VISIBLE) -->
<div class="col-span-full">
<.input
field={@form[:snmpv3_security_level]}
type="select"
label={t("Auth Level")}
options={[
{"No Authentication, No Privacy", "noAuthNoPriv"},
{"Authentication, No Privacy", "authNoPriv"},
{"Authentication + Privacy", "authPriv"}
]}
/>
</div>
<!-- Auth fields (shown for authNoPriv and authPriv) -->
<%= if selected_security_level in ["authNoPriv", "authPriv"] do %>
<div class="col-span-full">
<.input
field={@form[:snmpv3_auth_protocol]}
type="select"
label={t("Auth Protocol")}
options={[
{"SHA-256 (recommended)", "SHA-256"},
{"SHA-512", "SHA-512"},
{"SHA-384", "SHA-384"},
{"SHA-224", "SHA-224"},
{"SHA (SHA-1)", "SHA"},
{"MD5", "MD5"}
]}
/>
</div>
<div class="col-span-full">
<.input
field={@form[:snmpv3_auth_password]}
type="password"
label={t("Auth Password")}
placeholder={t("Min 8 characters")}
autocomplete="off"
/>
</div>
<% end %>
<!-- Privacy fields (shown for authPriv only) -->
<%= if selected_security_level == "authPriv" do %>
<div class="col-span-full">
<.input
field={@form[:snmpv3_priv_protocol]}
type="select"
label={t("Privacy Protocol")}
options={[
{"AES-128 (recommended)", "AES"},
{"AES-256", "AES-256"},
{"AES-192", "AES-192"},
{"AES-256-C", "AES-256-C"},
{"DES (legacy)", "DES"}
]}
/>
</div>
<div class="col-span-full">
<.input
field={@form[:snmpv3_priv_password]}
type="password"
label={t("Privacy Password")}
placeholder={t("Min 8 characters")}
autocomplete="off"
/>
</div>
<% end %>
<% end %>
<div class="col-span-full">
<.input
field={@form[:snmp_port]}
type="number"
label={t("SNMP Port")}
min="1"
max="65535"
value={@form[:snmp_port].value || 161}
/>
</div>
<div class="col-span-full">
<div class="flex gap-3">
<.button
type="button"
phx-click="test_snmp"
phx-disable-with={t("Testing...")}
>
{t("Test SNMP Connection")}
</.button>
<%= if @live_action == :edit do %>
<.button
type="button"
phx-click="trigger_discovery"
phx-disable-with={t("Discovering...")}
>
<.icon name="hero-magnifying-glass" class="h-4 w-4" /> Rediscover Device
</.button>
<% end %>
</div>
<%= if @snmp_test_result do %>
<%= cond do %>
<% Map.get(@snmp_test_result, :testing) -> %>
<div class="mt-3 rounded-md bg-blue-50 p-4 dark:bg-blue-900/20">
<div class="flex items-center">
<div class="shrink-0">
<.icon
name="hero-arrow-path"
class="h-5 w-5 text-blue-400 animate-spin"
/>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700 dark:text-blue-400">
{t("Testing SNMP credentials...")}
</p>
</div>
</div>
</div>
<% Map.get(@snmp_test_result, :success) -> %>
<div class="mt-3 rounded-md bg-green-50 p-4 dark:bg-green-900/20">
<div class="flex items-start">
<div class="shrink-0">
<.icon name="hero-check-circle" class="h-5 w-5 text-green-400" />
</div>
<div class="ml-3">
<p class="text-sm font-medium text-green-800 dark:text-green-400">
{t("Connection successful!")}
</p>
<p class="mt-1 text-sm text-green-700 dark:text-green-300">
{Map.get(@snmp_test_result, :message)}
</p>
</div>
</div>
</div>
<% true -> %>
<div class="mt-3 rounded-md bg-red-50 p-4 dark:bg-red-900/20">
<div class="flex items-start">
<div class="shrink-0">
<.icon name="hero-x-circle" class="h-5 w-5 text-red-400" />
</div>
<div class="ml-3">
<p class="text-sm font-medium text-red-800 dark:text-red-400">
{t("Connection failed")}
</p>
<p class="mt-1 text-sm text-red-700 dark:text-red-300">
{Map.get(@snmp_test_result, :message)}
</p>
</div>
</div>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
</div>
</div>
<%= if @live_action == :edit and @is_mikrotik_device do %>
<!-- MikroTik API Configuration -->
<div
class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8"
phx-hook="MikrotikPortSync"
id="mikrotik-config-section"
>
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("MikroTik API Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Configure MikroTik RouterOS API access. Works alongside SNMP for enhanced device management. SSH is used for device backups."
)}
</p>
<div class="mt-3 rounded-md bg-blue-50 dark:bg-blue-900/20 p-3">
<p class="text-xs text-blue-700 dark:text-blue-300">
<.icon name="hero-beaker" class="h-4 w-4 inline" />
<strong>{t("Experimental Feature:")}</strong>
{t("MikroTik API integration is under active development.")}
</p>
</div>
</div>
<div class="md:col-span-2">
<div class="space-y-6">
<%= if Map.has_key?(assigns, :mikrotik_config_source) and @mikrotik_config_source != :device do %>
<div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-4">
<p class="text-sm text-blue-700 dark:text-blue-300">
<strong>Inheriting credentials from {@mikrotik_config_source}.</strong>
Username: <span class="font-mono">{@effective_mikrotik_username}</span>
</p>
</div>
<% end %>
<div class="grid grid-cols-1 gap-x-6 gap-y-4 sm:max-w-xl sm:grid-cols-6">
<div class="col-span-full">
<.input
field={@form[:mikrotik_enabled]}
type="checkbox"
label={t("Enable MikroTik API")}
/>
</div>
<%= if @form[:mikrotik_enabled].value == true do %>
<div class="col-span-full">
<.input
field={@form[:mikrotik_username]}
label={t("Username")}
placeholder={t("Leave blank to inherit from site/organization")}
/>
</div>
<div class="col-span-full">
<.input
field={@form[:mikrotik_password]}
type="password"
label={t("Password")}
placeholder={t("Leave blank to inherit from site/organization")}
/>
</div>
<div class="col-span-full">
<.input
field={@form[:mikrotik_port]}
type="number"
label={t("API Port")}
placeholder="8729 (SSL) or 8728 (plain)"
/>
</div>
<div class="col-span-full">
<.input
field={@form[:mikrotik_use_ssl]}
type="checkbox"
label={t("Use SSL (API-SSL)")}
/>
</div>
<%= if @form[:mikrotik_use_ssl].value == false do %>
<div class="col-span-full">
<div class="rounded-md bg-red-50 dark:bg-red-900/20 p-4">
<p class="text-sm text-red-700 dark:text-red-300">
<strong>🚨 Critical Security Warning:</strong>
{t(
"Plain API (port 8728) sends credentials unencrypted over the network."
)}
<strong>
{t("This setting is blocked when using cloud pollers")}
</strong>
and should only be used with local agents on trusted networks.
</p>
</div>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
</div>
<% end %>
</div>
<!-- Save/Cancel Footer -->
<div class="flex items-center justify-between gap-x-6 border-t border-gray-200 px-4 py-4 sm:px-8 dark:border-white/10">
<.button navigate={~p"/devices"}>{t("Cancel")}</.button>
<.button
phx-disable-with={t("Saving...")}
variant="primary"
disabled={@duplicate_device != nil or @non_routable_ip_error}
>
{t("Save Device")}
</.button>
</div>
</.form>
<!-- Danger Zone -->
<%= if @live_action == :edit do %>
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8 border-t border-gray-200 dark:border-white/10">
<div>
<h2 class="text-base/7 font-semibold text-red-600 dark:text-red-500">
{t("Danger Zone")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Permanently delete this device and all its data.")}
</p>
</div>
<div class="md:col-span-2">
<div class="rounded-md bg-red-50 dark:bg-red-900/20 p-4 border border-red-200 dark:border-red-800">
<p class="text-sm text-red-800 dark:text-red-200 mb-4">
{t(
"Once you delete this device, there is no going back. All monitoring history and alerts will be permanently deleted."
)}
</p>
<.button
phx-click="delete"
data-confirm={
t(
"Are you sure you want to delete this device? All monitoring history and alerts will be permanently deleted."
)
}
variant="danger"
>
<.icon name="hero-trash" class="h-4 w-4" /> Delete Device
</.button>
</div>
</div>
</div>
<% end %>
</div>
</Layouts.authenticated>