Temporarily disable all MikroTik-related UI elements using false guards. Code is preserved and can be re-enabled by removing the guards. Hidden: org settings tab/panel, device form config section, device show backups/timeline tabs, site form config section, help page nav item.
779 lines
34 KiB
Text
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 false and @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>
|