better discovery
This commit is contained in:
parent
96fc488023
commit
639ffd4a2c
10 changed files with 316 additions and 17 deletions
|
|
@ -78,7 +78,7 @@ const SensorChart: SensorChartHook = {
|
|||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const unit = this.el.dataset.unit || '%'
|
||||
const unit = this.el.dataset.unit ?? '%'
|
||||
const autoScale = this.el.dataset.autoScale === 'true' || this.el.dataset.autoScale === 'true'
|
||||
const showZeroLine = this.el.dataset.showZeroLine === 'true' || this.el.dataset.showZeroLine === 'true'
|
||||
const range = this.el.dataset.range || '24h'
|
||||
|
|
|
|||
|
|
@ -426,6 +426,8 @@ defmodule Towerops.Profiles.YamlProfiles do
|
|||
base_oid: base_oid,
|
||||
oid_name: oid_name,
|
||||
sensor_descr: extract_descr(sensor_def, sensor_type),
|
||||
# Store the descr MIB OID reference for walking to get actual names
|
||||
descr_oid: extract_descr_oid(sensor_def),
|
||||
sensor_unit: Map.get(sensor_def, "unit"),
|
||||
sensor_divisor: Map.get(sensor_def, "divisor", 1),
|
||||
precision: Map.get(sensor_def, "precision", 1),
|
||||
|
|
@ -458,6 +460,22 @@ defmodule Towerops.Profiles.YamlProfiles do
|
|||
end
|
||||
end
|
||||
|
||||
# Extract the descr MIB OID reference when descr contains a MIB symbolic name
|
||||
# This allows the dynamic profile to walk the descr OID and get actual sensor names
|
||||
defp extract_descr_oid(sensor_def) do
|
||||
case Map.get(sensor_def, "descr") do
|
||||
descr when is_binary(descr) ->
|
||||
# Remove template variables first
|
||||
cleaned = descr |> String.replace(~r/\{\{ [^}]+ \}\}/, "") |> String.trim()
|
||||
|
||||
# If it's a MIB symbolic name (contains "::"), return it for later resolution
|
||||
if String.contains?(cleaned, "::"), do: cleaned
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Extract processor/CPU sensors (like LibreNMS processors module)
|
||||
defp extract_processor_oids(yaml) do
|
||||
case get_in(yaml, ["modules", "processors", "data"]) do
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ defmodule Towerops.Snmp do
|
|||
alias Towerops.Snmp.Sensor
|
||||
alias Towerops.Snmp.SensorReading
|
||||
alias Towerops.Snmp.StateSensor
|
||||
alias Towerops.Snmp.Storage
|
||||
alias Towerops.Snmp.Vlan
|
||||
|
||||
@doc """
|
||||
|
|
@ -99,7 +100,7 @@ defmodule Towerops.Snmp do
|
|||
def get_device_with_associations(device_id) do
|
||||
Device
|
||||
|> where([d], d.device_id == ^device_id)
|
||||
|> preload([:interfaces, :sensors, :state_sensors, :processors])
|
||||
|> preload([:interfaces, :sensors, :state_sensors, :processors, :storage])
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
|
|
@ -633,4 +634,32 @@ defmodule Towerops.Snmp do
|
|||
|> ProcessorReading.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
# Storage queries
|
||||
|
||||
@doc """
|
||||
Lists all storage entries for a device.
|
||||
"""
|
||||
def list_storage(snmp_device_id) do
|
||||
Storage
|
||||
|> where([s], s.snmp_device_id == ^snmp_device_id)
|
||||
|> order_by([s], s.storage_index)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a specific storage entry.
|
||||
"""
|
||||
def get_storage(storage_id) do
|
||||
Repo.get(Storage, storage_id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a storage entry.
|
||||
"""
|
||||
def update_storage(storage, attrs) do
|
||||
storage
|
||||
|> Storage.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ defmodule Towerops.Snmp.Device do
|
|||
alias Towerops.Snmp.Processor
|
||||
alias Towerops.Snmp.Sensor
|
||||
alias Towerops.Snmp.StateSensor
|
||||
alias Towerops.Snmp.Storage
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
|
@ -38,6 +39,7 @@ defmodule Towerops.Snmp.Device do
|
|||
has_many :processors, Processor, foreign_key: :snmp_device_id
|
||||
has_many :sensors, Sensor, foreign_key: :snmp_device_id
|
||||
has_many :state_sensors, StateSensor, foreign_key: :snmp_device_id
|
||||
has_many :storage, Storage, foreign_key: :snmp_device_id
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
|
@ -62,6 +64,7 @@ defmodule Towerops.Snmp.Device do
|
|||
processors: NotLoaded.t() | [Processor.t()],
|
||||
sensors: NotLoaded.t() | [Sensor.t()],
|
||||
state_sensors: NotLoaded.t() | [StateSensor.t()],
|
||||
storage: NotLoaded.t() | [Storage.t()],
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ defmodule Towerops.Snmp.Discovery do
|
|||
alias Towerops.Snmp.Profiles.Base
|
||||
alias Towerops.Snmp.Profiles.Dynamic
|
||||
alias Towerops.Snmp.Sensor
|
||||
alias Towerops.Snmp.Storage
|
||||
alias Towerops.Snmp.Vlan
|
||||
|
||||
require Logger
|
||||
|
|
@ -131,9 +132,11 @@ defmodule Towerops.Snmp.Discovery do
|
|||
{:ok, vlans} <- discover_vlans_with_timeout(client_opts, profile, timeouts),
|
||||
{:ok, ip_addresses} <- discover_ip_addresses_with_timeout(client_opts, timeouts),
|
||||
{:ok, processors} <- discover_processors_with_timeout(client_opts, timeouts),
|
||||
{:ok, storage} <- discover_storage_with_timeout(client_opts, timeouts),
|
||||
{:ok, discovered_device} <- save_discovery_results(device, device_info, interfaces, sensors, vlans),
|
||||
:ok <- sync_ip_addresses(discovered_device, ip_addresses),
|
||||
:ok <- sync_processors(discovered_device, processors),
|
||||
:ok <- sync_storage(discovered_device, storage),
|
||||
{:ok, neighbors} <- discover_neighbors_with_timeout(client_opts, discovered_device.interfaces, timeouts),
|
||||
:ok <- save_neighbors(discovered_device.device_id, neighbors) do
|
||||
_ = update_device_discovery_time(device)
|
||||
|
|
@ -222,6 +225,16 @@ defmodule Towerops.Snmp.Discovery do
|
|||
)
|
||||
end
|
||||
|
||||
# Storage discovery with timeout - falls back to empty list on timeout
|
||||
defp discover_storage_with_timeout(client_opts, timeouts) do
|
||||
DeferredDiscovery.slow_check(
|
||||
client_opts,
|
||||
fn -> Base.discover_storage(client_opts) end,
|
||||
timeout: timeouts[:slow],
|
||||
default: []
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs discovery for all SNMP-enabled devices in an organization.
|
||||
Returns a summary of successful and failed discoveries.
|
||||
|
|
@ -762,6 +775,63 @@ defmodule Towerops.Snmp.Discovery do
|
|||
:ok
|
||||
end
|
||||
|
||||
@spec sync_storage(Device.t(), [map()]) :: :ok
|
||||
defp sync_storage(device, discovered_storage) do
|
||||
# Get existing storage for this device, indexed by storage_index
|
||||
existing_storage =
|
||||
from(s in Storage, where: s.snmp_device_id == ^device.id)
|
||||
|> Repo.all()
|
||||
|> Map.new(&{&1.storage_index, &1})
|
||||
|
||||
# Get the set of discovered storage_indices (convert string index to integer)
|
||||
discovered_indices =
|
||||
MapSet.new(discovered_storage, fn s ->
|
||||
normalize_storage_index(s.storage_index)
|
||||
end)
|
||||
|
||||
# Delete storage that no longer exists on the device
|
||||
existing_indices = MapSet.new(Map.keys(existing_storage))
|
||||
removed_indices = MapSet.difference(existing_indices, discovered_indices)
|
||||
|
||||
if MapSet.size(removed_indices) > 0 do
|
||||
removed_list = MapSet.to_list(removed_indices)
|
||||
|
||||
Repo.delete_all(
|
||||
from s in Storage,
|
||||
where: s.snmp_device_id == ^device.id and s.storage_index in ^removed_list
|
||||
)
|
||||
|
||||
Logger.debug("Deleted #{MapSet.size(removed_indices)} removed storage entries")
|
||||
end
|
||||
|
||||
# Upsert each discovered storage
|
||||
Enum.each(discovered_storage, fn storage_data ->
|
||||
storage_index = normalize_storage_index(storage_data.storage_index)
|
||||
storage_attrs = Map.put(storage_data, :storage_index, storage_index)
|
||||
|
||||
case Map.get(existing_storage, storage_index) do
|
||||
nil ->
|
||||
# New storage - insert
|
||||
%Storage{}
|
||||
|> Storage.changeset(Map.put(storage_attrs, :snmp_device_id, device.id))
|
||||
|> Repo.insert!()
|
||||
|
||||
existing ->
|
||||
# Existing storage - update
|
||||
existing
|
||||
|> Storage.changeset(storage_attrs)
|
||||
|> Repo.update!()
|
||||
end
|
||||
end)
|
||||
|
||||
Logger.info("Synced #{length(discovered_storage)} storage entries for device")
|
||||
:ok
|
||||
end
|
||||
|
||||
# Normalize storage index to integer (Base.discover_storage may return string from OID parsing)
|
||||
defp normalize_storage_index(index) when is_integer(index), do: index
|
||||
defp normalize_storage_index(index) when is_binary(index), do: String.to_integer(index)
|
||||
|
||||
@spec update_device_discovery_time(DeviceSchema.t()) ::
|
||||
{:ok, DeviceSchema.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp update_device_discovery_time(device) do
|
||||
|
|
|
|||
|
|
@ -55,6 +55,17 @@ defmodule Towerops.Snmp.MibTranslator do
|
|||
"MIKROTIK-MIB::mtxrHlBackupPowerSupplyState.0" => "1.3.6.1.4.1.14988.1.1.3.16.0",
|
||||
"MIKROTIK-MIB::mtxrHlFanSpeed1.0" => "1.3.6.1.4.1.14988.1.1.3.17.0",
|
||||
"MIKROTIK-MIB::mtxrHlFanSpeed2.0" => "1.3.6.1.4.1.14988.1.1.3.18.0",
|
||||
# MikroTik Gauge Table (mtxrHealth.100) - sensor names and values
|
||||
"MIKROTIK-MIB::mtxrGaugeName" => "1.3.6.1.4.1.14988.1.1.3.100.1.2",
|
||||
"MIKROTIK-MIB::mtxrGaugeValue" => "1.3.6.1.4.1.14988.1.1.3.100.1.3",
|
||||
"MIKROTIK-MIB::mtxrGaugeUnit" => "1.3.6.1.4.1.14988.1.1.3.100.1.4",
|
||||
# MikroTik Optical Table (mtxrOptical) - SFP sensor names and values
|
||||
"MIKROTIK-MIB::mtxrOpticalName" => "1.3.6.1.4.1.14988.1.1.19.1.1.2",
|
||||
"MIKROTIK-MIB::mtxrOpticalTemperature" => "1.3.6.1.4.1.14988.1.1.19.1.1.6",
|
||||
"MIKROTIK-MIB::mtxrOpticalSupplyVoltage" => "1.3.6.1.4.1.14988.1.1.19.1.1.7",
|
||||
"MIKROTIK-MIB::mtxrOpticalTxBiasCurrent" => "1.3.6.1.4.1.14988.1.1.19.1.1.8",
|
||||
"MIKROTIK-MIB::mtxrOpticalTxPower" => "1.3.6.1.4.1.14988.1.1.19.1.1.9",
|
||||
"MIKROTIK-MIB::mtxrOpticalRxPower" => "1.3.6.1.4.1.14988.1.1.19.1.1.10",
|
||||
# CISCO-ENTITY-SENSOR-MIB (enterprise 9.9.91)
|
||||
"CISCO-ENTITY-SENSOR-MIB::entSensorType" => "1.3.6.1.4.1.9.9.91.1.1.1.1.1",
|
||||
"CISCO-ENTITY-SENSOR-MIB::entSensorScale" => "1.3.6.1.4.1.9.9.91.1.1.1.1.2",
|
||||
|
|
|
|||
|
|
@ -209,39 +209,66 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
|
||||
defp walk_table_sensor(sensor_def, client_opts) do
|
||||
base_oid = sensor_def[:base_oid]
|
||||
descr_oid = sensor_def[:descr_oid]
|
||||
|
||||
case Client.walk(client_opts, base_oid) do
|
||||
{:ok, results} when is_map(results) and map_size(results) > 0 ->
|
||||
parse_table_walk_results(results, sensor_def)
|
||||
# If there's a descr_oid, walk it to get actual sensor names
|
||||
descr_map = walk_descr_oid(descr_oid, client_opts)
|
||||
parse_table_walk_results(results, sensor_def, descr_map)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_table_walk_results(results, sensor_def) when is_map(results) do
|
||||
# Walk the description OID to get actual sensor names (e.g., mtxrGaugeName)
|
||||
defp walk_descr_oid(nil, _client_opts), do: %{}
|
||||
|
||||
defp walk_descr_oid(descr_mib_name, client_opts) do
|
||||
with {:ok, numeric_oid} <- MibTranslator.translate(descr_mib_name),
|
||||
{:ok, results} when is_map(results) <- Client.walk(client_opts, numeric_oid) do
|
||||
# Build a map of index -> description
|
||||
Map.new(results, fn {oid, value} ->
|
||||
# Extract the index from the full OID (last component)
|
||||
index = oid |> String.split(".") |> List.last()
|
||||
{index, to_string(value)}
|
||||
end)
|
||||
else
|
||||
{:error, :translation_failed} ->
|
||||
Logger.debug("Failed to translate descr MIB name: #{descr_mib_name}")
|
||||
%{}
|
||||
|
||||
_ ->
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_table_walk_results(results, sensor_def, descr_map) when is_map(results) do
|
||||
results
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {{oid, value}, idx} ->
|
||||
build_table_sensor(sensor_def, oid, value, idx)
|
||||
# Extract the index from the OID to look up the description
|
||||
oid_index = oid |> String.split(".") |> List.last()
|
||||
build_table_sensor(sensor_def, oid, value, idx, descr_map, oid_index)
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
# Build a sensor from table walk result
|
||||
defp build_table_sensor(sensor_def, oid, value, idx) when is_integer(value) do
|
||||
defp build_table_sensor(sensor_def, oid, value, idx, descr_map, oid_index) when is_integer(value) do
|
||||
%{
|
||||
sensor_type: sensor_def[:sensor_type],
|
||||
sensor_index: "#{sensor_def[:sensor_type]}_#{idx}",
|
||||
sensor_oid: oid,
|
||||
sensor_descr: build_sensor_descr(sensor_def, idx),
|
||||
sensor_descr: build_sensor_descr(sensor_def, idx, descr_map, oid_index),
|
||||
sensor_unit: sensor_def[:sensor_unit] || "",
|
||||
sensor_divisor: sensor_def[:sensor_divisor] || 1,
|
||||
last_value: value / 1
|
||||
}
|
||||
end
|
||||
|
||||
defp build_table_sensor(_sensor_def, _oid, _value, _idx), do: nil
|
||||
defp build_table_sensor(_sensor_def, _oid, _value, _idx, _descr_map, _oid_index), do: nil
|
||||
|
||||
# Discover state sensors (sensors with discrete states like GPS Status, DFS Status)
|
||||
defp discover_state_sensors(sensor_defs, client_opts) do
|
||||
|
|
@ -279,7 +306,7 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
sensor_type: "state",
|
||||
sensor_index: "#{sensor_def[:oid_name] || "state"}_#{idx}",
|
||||
sensor_oid: oid,
|
||||
sensor_descr: build_sensor_descr(sensor_def, idx),
|
||||
sensor_descr: build_sensor_descr_default(sensor_def, idx),
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1,
|
||||
last_value: value / 1,
|
||||
|
|
@ -289,7 +316,21 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
|
||||
defp build_state_sensor(_sensor_def, _oid, _value, _idx), do: nil
|
||||
|
||||
defp build_sensor_descr(sensor_def, idx) do
|
||||
# Build sensor description, using the descr_map if available for actual device names
|
||||
defp build_sensor_descr(sensor_def, idx, descr_map, oid_index) do
|
||||
# First, try to get the description from the descr_map (walked from device)
|
||||
case Map.get(descr_map, oid_index) do
|
||||
descr when is_binary(descr) and descr != "" ->
|
||||
# Use the actual device name directly
|
||||
descr
|
||||
|
||||
_ ->
|
||||
# Fall back to the default description building logic
|
||||
build_sensor_descr_default(sensor_def, idx)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_sensor_descr_default(sensor_def, idx) do
|
||||
base_descr = sensor_def[:sensor_descr] || sensor_def[:oid_name] || sensor_def[:sensor_type]
|
||||
|
||||
# If the description contains a MIB symbolic name (e.g., "MIKROTIK-MIB::mtxrOpticalName"),
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
vlans = load_vlans(snmp_data.device)
|
||||
ip_addresses = load_ip_addresses(snmp_data.device)
|
||||
processors = load_processors(snmp_data.device)
|
||||
storage = load_storage(snmp_data.device)
|
||||
latency_chart_data = load_latency_chart_data(device_id)
|
||||
processor_chart_data = load_processor_chart_data(processors)
|
||||
memory_chart_data = load_sensor_chart_data(device_id, ["memory_usage"])
|
||||
|
|
@ -186,6 +187,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
|> assign(:ipv4_addresses, Enum.filter(ip_addresses, &(&1.ip_type == "ipv4")))
|
||||
|> assign(:ipv6_addresses, Enum.filter(ip_addresses, &(&1.ip_type == "ipv6")))
|
||||
|> assign(:processors, processors)
|
||||
|> assign(:storage, storage)
|
||||
|> assign(:events, events)
|
||||
|> assign(:latency_chart_data, latency_chart_data)
|
||||
|> assign(:processor_chart_data, processor_chart_data)
|
||||
|
|
@ -238,6 +240,30 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
|
||||
defp format_speed(_), do: "-"
|
||||
|
||||
defp format_bytes(nil), do: "-"
|
||||
defp format_bytes(0), do: "0 B"
|
||||
|
||||
defp format_bytes(bytes) when is_integer(bytes) do
|
||||
cond do
|
||||
bytes >= 1_099_511_627_776 ->
|
||||
"#{Float.round(bytes / 1_099_511_627_776, 1)} TB"
|
||||
|
||||
bytes >= 1_073_741_824 ->
|
||||
"#{Float.round(bytes / 1_073_741_824, 1)} GB"
|
||||
|
||||
bytes >= 1_048_576 ->
|
||||
"#{Float.round(bytes / 1_048_576, 1)} MB"
|
||||
|
||||
bytes >= 1024 ->
|
||||
"#{Float.round(bytes / 1024, 1)} KB"
|
||||
|
||||
true ->
|
||||
"#{bytes} B"
|
||||
end
|
||||
end
|
||||
|
||||
defp format_bytes(_), do: "-"
|
||||
|
||||
defp format_sensor_value(value, _divisor) when is_number(value) do
|
||||
# Value is already divided by divisor when stored in sensor_reading
|
||||
Float.round(value, 1)
|
||||
|
|
@ -365,6 +391,9 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
defp load_processors(nil), do: []
|
||||
defp load_processors(snmp_device), do: Snmp.list_processors(snmp_device.id)
|
||||
|
||||
defp load_storage(nil), do: []
|
||||
defp load_storage(snmp_device), do: Snmp.list_storage(snmp_device.id)
|
||||
|
||||
defp load_processor_chart_data([]), do: nil
|
||||
|
||||
defp load_processor_chart_data(processors) do
|
||||
|
|
|
|||
|
|
@ -435,7 +435,9 @@
|
|||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||
<dt class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<.link
|
||||
navigate={~p"/devices/#{@device.id}/graph/storage"}
|
||||
navigate={
|
||||
~p"/devices/#{@device.id}/graph/storage?sensor_id=#{sensor.id}"
|
||||
}
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
||||
>
|
||||
{sensor.sensor_descr}
|
||||
|
|
@ -458,6 +460,60 @@
|
|||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- Physical Storage Volumes (from HOST-RESOURCES-MIB) -->
|
||||
<%= if @storage && length(@storage) > 0 do %>
|
||||
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
|
||||
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Storage Volumes
|
||||
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||
({length(@storage)})
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4 space-y-4">
|
||||
<%= for storage <- @storage do %>
|
||||
<% usage_percent =
|
||||
if storage.total_bytes && storage.total_bytes > 0 do
|
||||
Float.round(storage.used_bytes / storage.total_bytes * 100, 1)
|
||||
else
|
||||
0.0
|
||||
end %>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{storage.description || "Storage #{storage.storage_index}"}
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{format_bytes(storage.used_bytes)} / {format_bytes(storage.total_bytes)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1 bg-gray-200 rounded-full h-2 dark:bg-gray-700">
|
||||
<div
|
||||
class={[
|
||||
"h-2 rounded-full transition-all",
|
||||
usage_percent < 70 && "bg-green-500",
|
||||
usage_percent >= 70 && usage_percent < 90 && "bg-yellow-500",
|
||||
usage_percent >= 90 && "bg-red-500"
|
||||
]}
|
||||
style={"width: #{min(usage_percent, 100)}%"}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400 w-12 text-right">
|
||||
{usage_percent}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{String.replace(storage.storage_type || "other", "_", " ")
|
||||
|> String.capitalize()}
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- Temperature Sensors -->
|
||||
<%= if @temperature_sensors && length(@temperature_sensors) > 0 do %>
|
||||
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
|
||||
|
|
@ -472,7 +528,9 @@
|
|||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||
<dt class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<.link
|
||||
navigate={~p"/devices/#{@device.id}/graph/temperature"}
|
||||
navigate={
|
||||
~p"/devices/#{@device.id}/graph/temperature?sensor_id=#{sensor.id}"
|
||||
}
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
||||
>
|
||||
{sensor.sensor_descr}
|
||||
|
|
@ -507,7 +565,9 @@
|
|||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||
<dt class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<.link
|
||||
navigate={~p"/devices/#{@device.id}/graph/voltage"}
|
||||
navigate={
|
||||
~p"/devices/#{@device.id}/graph/voltage?sensor_id=#{sensor.id}"
|
||||
}
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
||||
>
|
||||
{sensor.sensor_descr}
|
||||
|
|
@ -545,7 +605,9 @@
|
|||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||
<dt class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<.link
|
||||
navigate={~p"/devices/#{@device.id}/graph/dbm"}
|
||||
navigate={
|
||||
~p"/devices/#{@device.id}/graph/dbm?sensor_id=#{sensor.id}"
|
||||
}
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
||||
>
|
||||
{sensor.sensor_descr}
|
||||
|
|
@ -581,7 +643,9 @@
|
|||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||
<dt class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<.link
|
||||
navigate={~p"/devices/#{@device.id}/graph/count"}
|
||||
navigate={
|
||||
~p"/devices/#{@device.id}/graph/count?sensor_id=#{sensor.id}"
|
||||
}
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
||||
>
|
||||
{sensor.sensor_descr}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
|
||||
range = Map.get(params, "range", "24h")
|
||||
interface_id = Map.get(params, "interface_id")
|
||||
sensor_id = Map.get(params, "sensor_id")
|
||||
|
||||
socket =
|
||||
socket
|
||||
|
|
@ -27,6 +28,7 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
|> assign(:sensor_type, sensor_type)
|
||||
|> assign(:range, range)
|
||||
|> assign(:interface_id, interface_id)
|
||||
|> assign(:sensor_id, sensor_id)
|
||||
|> load_graph_data()
|
||||
|
||||
{:noreply, socket}
|
||||
|
|
@ -45,8 +47,15 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
defp build_graph_params(assigns, range) do
|
||||
base_params = %{"range" => range}
|
||||
|
||||
if assigns.interface_id do
|
||||
Map.put(base_params, "interface_id", assigns.interface_id)
|
||||
base_params =
|
||||
if assigns.interface_id do
|
||||
Map.put(base_params, "interface_id", assigns.interface_id)
|
||||
else
|
||||
base_params
|
||||
end
|
||||
|
||||
if assigns.sensor_id do
|
||||
Map.put(base_params, "sensor_id", assigns.sensor_id)
|
||||
else
|
||||
base_params
|
||||
end
|
||||
|
|
@ -59,6 +68,7 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
sensor_type = socket.assigns.sensor_type
|
||||
range = socket.assigns.range
|
||||
interface_id = socket.assigns.interface_id
|
||||
sensor_id = socket.assigns.sensor_id
|
||||
|
||||
device = Devices.get_device!(device_id)
|
||||
|
||||
|
|
@ -73,6 +83,10 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
sensor_type == "traffic" ->
|
||||
{load_traffic_chart_data(device_id, range), nil}
|
||||
|
||||
sensor_id ->
|
||||
# Load only the specific sensor when sensor_id is provided
|
||||
{load_single_sensor_chart_data(sensor_id, range), get_sensor_name(sensor_id)}
|
||||
|
||||
true ->
|
||||
sensor_types = get_sensor_types_for_chart(sensor_type)
|
||||
{load_sensor_chart_data(device_id, sensor_types, range), nil}
|
||||
|
|
@ -131,6 +145,26 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
end
|
||||
end
|
||||
|
||||
defp load_single_sensor_chart_data(sensor_id, range) do
|
||||
case Snmp.get_sensor(sensor_id) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
sensor ->
|
||||
since = get_datetime_from_range(range)
|
||||
limit = get_limit_for_range(range)
|
||||
dataset = sensor_to_dataset(sensor, since, limit)
|
||||
Jason.encode!(%{datasets: [dataset]})
|
||||
end
|
||||
end
|
||||
|
||||
defp get_sensor_name(sensor_id) do
|
||||
case Snmp.get_sensor(sensor_id) do
|
||||
nil -> nil
|
||||
sensor -> sensor.sensor_descr
|
||||
end
|
||||
end
|
||||
|
||||
defp build_sensor_chart_json(device, sensor_types, range) do
|
||||
sensors =
|
||||
device.sensors
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue