From df295838c78a157681dc61d79e68c8e363e43efe Mon Sep 17 00:00:00 2001 From: Elin Olsson Date: Thu, 5 Sep 2024 09:38:53 +0200 Subject: [PATCH] Device health liveview (#1484) * Add liveview with simple metrics graphs * Add time frame options for metrics * Add basic tests for health liveview --- assets/css/_custom.scss | 20 +- assets/css/_metrics.scss | 89 +++++++ assets/css/app.scss | 3 +- lib/nerves_hub/devices.ex | 14 +- .../components/device_health/health_header.ex | 102 ++++++++ .../device_health/health_section.ex | 22 ++ .../live/devices/device_health.ex | 239 ++++++++++++++++++ .../live/devices/device_health.html.heex | 50 ++++ .../live/devices/show.html.heex | 17 +- lib/nerves_hub_web/router.ex | 5 + mix.exs | 1 + mix.lock | 2 + test/nerves_hub/devices_test.exs | 4 +- .../workers/device_health_truncation_test.exs | 2 +- .../live/devices/health_test.exs | 110 ++++++++ 15 files changed, 660 insertions(+), 20 deletions(-) create mode 100644 assets/css/_metrics.scss create mode 100644 lib/nerves_hub_web/components/device_health/health_header.ex create mode 100644 lib/nerves_hub_web/components/device_health/health_section.ex create mode 100644 lib/nerves_hub_web/live/devices/device_health.ex create mode 100644 lib/nerves_hub_web/live/devices/device_health.html.heex create mode 100644 test/nerves_hub_web/live/devices/health_test.exs diff --git a/assets/css/_custom.scss b/assets/css/_custom.scss index 9c965eae1..253929aa6 100644 --- a/assets/css/_custom.scss +++ b/assets/css/_custom.scss @@ -65,7 +65,7 @@ margin-top: 0; } - & > img { + &>img { margin: auto; opacity: .75; } @@ -76,6 +76,7 @@ margin-top: .5rem; } } + .options .dropdown-item { color: var(--white-50); transition: color 200ms ease; @@ -114,7 +115,7 @@ display: none; } - & + .dropdown-menu { + &+.dropdown-menu { padding: 0; border-radius: 4px; background-color: var(--background); @@ -230,7 +231,7 @@ grid-column-gap: 3rem; grid-row-gap: 2rem; - & > div:first-child { + &>div:first-child { grid-column: span 2; } @@ -257,16 +258,16 @@ } html { - .btn-group > form:not(:first-child) .btn { + .btn-group>form:not(:first-child) .btn { margin-left: -1px; } - .btn-group > form:not(:last-child) .btn { + .btn-group>form:not(:last-child) .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; } - .btn-group > form:not(:first-child) .btn { + .btn-group>form:not(:first-child) .btn { border-top-left-radius: 0; border-bottom-left-radius: 0; } @@ -285,7 +286,7 @@ html { align-items: center; margin-bottom: 1.5rem; - & + h1 { + &+h1 { margin-top: -1.5rem; } @@ -296,7 +297,7 @@ html { @media(max-width: 860px) { margin-bottom: 1rem; - & + h1 { + &+h1 { margin-top: 0; } } @@ -337,9 +338,10 @@ html { .device-header-group { display: flex; + .btn { margin-left: 1%; height: fit-content; min-width: min-content; } -} +} \ No newline at end of file diff --git a/assets/css/_metrics.scss b/assets/css/_metrics.scss new file mode 100644 index 000000000..fc766b6a5 --- /dev/null +++ b/assets/css/_metrics.scss @@ -0,0 +1,89 @@ +.metrics-section { + background: var(--backgroundLight); + padding: 1.5rem; + border-radius: 4px; + margin-bottom: 2rem; +} + + +.metrics-active { + background-color: rgb(68, 66, 66); +} + +.btn.btn-outline-light.metrics-active:hover { + background-color: rgb(68, 66, 66); +} + +.metrics-btn-group .btn.btn-outline-light:focus { + box-shadow: none; +} + +.metrics-text { + margin-top: 2rem; + margin-bottom: 2rem; +} + + +// Contex Plots + +/* Styling for tick line */ +.exc-tick { + stroke: rgb(236, 235, 235); +} + +.exc-tick line { + stroke: rgb(236, 235, 235); +} + +/* Styling for tick text */ +.exc-tick text { + fill: rgb(236, 235, 235); + stroke: none; +} + +/* Styling for axis line */ +.exc-domain { + stroke: rgb(207, 207, 207); +} + +/* Styling for grid line */ +.exc-grid { + stroke: lightgrey; +} + +/* Styling for outline of colours in legend */ +.exc-legend { + stroke: black; +} + +/* Styling for text of colours in legend */ +.exc-legend text { + fill: grey; + font-size: 0.8rem; + stroke: none; +} + +/* Styling for title & subtitle of any plot */ +.exc-title { + fill: darkslategray; + font-size: 2.3rem; + stroke: none; +} + +.exc-subtitle { + fill: darkgrey; + font-size: 1.0rem; + stroke: none; +} + +/* Styling for label printed inside a bar on a barchart */ +.exc-barlabel-in { + fill: white; + font-size: 0.7rem; +} + +/* Styling for label printed outside of a bar (e.g. if bar is too small) */ +.exc-barlabel-out { + fill: grey; + font-size: 0.7rem; +} \ No newline at end of file diff --git a/assets/css/app.scss b/assets/css/app.scss index 2af3556cd..f4709304d 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -23,6 +23,7 @@ $fa-font-path: '~@fortawesome/fontawesome-free/webfonts'; @import 'modal'; @import 'import'; @import '~bootstrap/scss/bootstrap'; +@import 'metrics'; html { margin: 0; @@ -237,4 +238,4 @@ html body { .device-health .callout.danger { color: orangered; -} +} \ No newline at end of file diff --git a/lib/nerves_hub/devices.ex b/lib/nerves_hub/devices.ex index 9cecaf269..1b2882dea 100644 --- a/lib/nerves_hub/devices.ex +++ b/lib/nerves_hub/devices.ex @@ -1139,8 +1139,18 @@ defmodule NervesHub.Devices do end end - def get_all_health(device_id) do - from(DeviceHealth, where: [device_id: ^device_id]) + def get_device_health(device_id) do + DeviceHealth + |> where(device_id: ^device_id) + |> order_by(asc: :inserted_at) + |> Repo.all() + end + + def get_device_health(device_id, unit, amount) do + DeviceHealth + |> where(device_id: ^device_id) + |> where([d], d.inserted_at > ago(^amount, ^unit)) + |> order_by(asc: :inserted_at) |> Repo.all() end diff --git a/lib/nerves_hub_web/components/device_health/health_header.ex b/lib/nerves_hub_web/components/device_health/health_header.ex new file mode 100644 index 000000000..a01adb34b --- /dev/null +++ b/lib/nerves_hub_web/components/device_health/health_header.ex @@ -0,0 +1,102 @@ +defmodule NervesHubWeb.Components.HealthHeader do + use NervesHubWeb, :component + + attr(:org, :any) + attr(:product, :any) + attr(:device, :any) + attr(:status, :any) + attr(:latest_health, :any) + attr(:health_check_timer, :any, default: nil) + + def render(assigns) do + ~H""" +
+
+

Device Health

+

Device identifier: <%= @device.identifier %>

+
+
+ +
+
+
+
+
Status
+

+ <%= @status %> + + <%= if @status in ["offline"] do %> + offline + <% else %> + online + <% end %> + +

+
+
+
+ Last connected + + + <%= @device.connection_established_at %> + +
+

+ Never + +

+
+
+
+ Last reported + + + <%= @latest_health.inserted_at %> + +
+

+ Never + +

+
+
+
Version
+ <%= if is_nil(@device.firmware_metadata) do %> +

Unknown

+ <% else %> + <.link navigate={~p"/org/#{@org.name}/#{@product.name}/firmware/#{@device.firmware_metadata.uuid}"} class="badge ff-m mt-0"> + <%= @device.firmware_metadata.version %> (<%= String.slice(@device.firmware_metadata.uuid, 0..7) %>) + + <% end %> +
+
+
Platform
+ <%= if is_nil(@device.firmware_metadata.platform) do %> +

Unknown

+ <% else %> +

+ <%= @device.firmware_metadata.platform %> +

+ <% end %> +
+
+ """ + end +end diff --git a/lib/nerves_hub_web/components/device_health/health_section.ex b/lib/nerves_hub_web/components/device_health/health_section.ex new file mode 100644 index 000000000..79bb16a5c --- /dev/null +++ b/lib/nerves_hub_web/components/device_health/health_section.ex @@ -0,0 +1,22 @@ +defmodule NervesHubWeb.Components.HealthSection do + use NervesHubWeb, :component + + attr(:title, :string) + attr(:svg, :any) + attr(:memory_size, :any, default: nil) + attr(:memory_usage, :any, default: nil) + + def render(assigns) do + ~H""" +
+
+ <%= @title %> +
Currently using <%= @memory_usage %>% of <%= @memory_size %> MB.
+
+
+ <%= @svg %> +
+
+ """ + end +end diff --git a/lib/nerves_hub_web/live/devices/device_health.ex b/lib/nerves_hub_web/live/devices/device_health.ex new file mode 100644 index 000000000..71a0293e9 --- /dev/null +++ b/lib/nerves_hub_web/live/devices/device_health.ex @@ -0,0 +1,239 @@ +defmodule NervesHubWeb.Live.Devices.DeviceHealth do + use NervesHubWeb, :updated_live_view + + alias NervesHub.Devices + alias NervesHub.Tracker + + alias NervesHubWeb.Components.HealthHeader + alias NervesHubWeb.Components.HealthSection + + alias Phoenix.Socket.Broadcast + + @check_health_interval 60_000 + @time_frame_opts [ + {"hour", 1}, + {"day", 1}, + {"day", 7} + ] + @default_time_frame {"hour", 1} + @default_chart_type :scatter + + @metrics_structure %{ + cpu_temp: [], + load_15min: [], + load_1min: [], + load_5min: [], + size_mb: [], + used_mb: [], + used_percent: [] + } + + def mount(%{"device_identifier" => device_identifier}, _session, socket) do + %{org: org} = socket.assigns + + device = Devices.get_device_by_identifier!(org, device_identifier) + + if connected?(socket) do + socket.endpoint.subscribe("device:#{device.identifier}:internal") + end + + socket + |> page_title("Device #{device.identifier} - Health") + |> assign(:device, device) + |> assign(:status, Tracker.status(device)) + |> assign(:time_frame, @default_time_frame) + |> assign(:time_frame_opts, @time_frame_opts) + |> assign(:chart_type, @default_chart_type) + |> schedule_health_check_timer() + |> assign_metrics() + |> ok() + end + + def handle_event("set-time-frame", %{"unit" => unit, "amount" => amount}, socket) do + socket + |> assign(:time_frame, {unit, String.to_integer(amount)}) + |> assign_metrics() + |> noreply() + end + + def handle_event("scatter-chart", _, socket) do + socket + |> assign(:chart_type, :scatter) + |> assign_metrics() + |> noreply() + end + + def handle_event("line-chart", _, socket) do + socket + |> assign(:chart_type, :line) + |> assign_metrics() + |> noreply() + end + + def handle_event("toggle-health-check-auto-refresh", _value, socket) do + if timer_ref = socket.assigns.health_check_timer do + _ = Process.cancel_timer(timer_ref) + {:noreply, assign(socket, :health_check_timer, nil)} + else + {:noreply, schedule_health_check_timer(socket)} + end + end + + def handle_info(:check_health_interval, socket) do + timer_ref = Process.send_after(self(), :check_health_interval, @check_health_interval) + + socket.endpoint.broadcast("device:#{socket.assigns.device.id}", "check_health", %{}) + + socket + |> assign(:health_check_timer, timer_ref) + |> noreply() + end + + def handle_info(%Broadcast{event: "health_check_report"}, socket) do + socket + |> assign_metrics() + |> noreply() + end + + # Ignore other events for now + def handle_info(_event, socket), do: {:noreply, socket} + + @doc """ + Organizes health data into metrics structure suitable for Contex plots. + """ + def organize_data(health) do + Enum.reduce(health, @metrics_structure, fn h, acc -> + metrics = h.data["metrics"] + + if metrics do + ts = NaiveDateTime.from_iso8601!(h.data["timestamp"]) + + acc + |> Map.keys() + |> Enum.reduce(acc, fn key, acc -> + str_key = to_string(key) + Map.put(acc, key, [[ts, metrics[str_key]] | acc[key]]) + end) + else + acc + end + end) + end + + defp assign_metrics( + %{ + assigns: %{ + device: device, + chart_type: chart_type, + time_frame: {unit, amount} = time_frame + } + } = socket + ) do + latest_health = Devices.get_latest_health(device.id) + + {memory_size, memory_usage} = + case latest_health do + %{data: %{"metrics" => metrics}} -> {metrics["size_mb"], metrics["used_percent"]} + _ -> {0, 0} + end + + metrics = + device.id + |> Devices.get_device_health(unit, amount) + |> organize_data() + + socket + |> assign(:latest_health, latest_health) + |> assign(:memory_size, memory_size) + |> assign(:memory_usage, memory_usage) + |> assign(:graphs, create_graphs(metrics, chart_type, memory_size, time_frame)) + end + + defp create_graphs(metrics, chart_type, memory_size, time_frame) do + metrics + |> Enum.reduce(%{}, fn {metric_type, data}, acc -> + case metric_type do + :size_mb -> + acc + + _ -> + max_value = get_max_value(metric_type, data, memory_size) + chart_svg = create_chart(data, chart_type, max_value, time_frame) + + Map.put(acc, metric_type, chart_svg) + end + end) + end + + defp get_max_value(_type, data, _memory_size) when data == [], do: 0 + + defp get_max_value(type, data, memory_size) do + case type do + :load_1min -> get_cpu_load_max_value(data) + :load_5min -> get_cpu_load_max_value(data) + :load_15min -> get_cpu_load_max_value(data) + :used_mb -> memory_size + _ -> 100 + end + end + + defp get_cpu_load_max_value(data) do + data + |> Enum.max_by(fn [_, value] -> value end) + |> List.last() + |> ceil() + |> max(1) + end + + defp create_chart(data, _chart_type, _max_value, _time_unit) + when data == [], + do: raw("

No data for selected period

") + + defp create_chart(data, chart_type, max_value, time_unit) do + now = NaiveDateTime.utc_now() + + x_scale = + Contex.TimeScale.new() + |> Contex.TimeScale.domain(time_scale_start(now, time_unit), now) + |> Contex.TimeScale.interval_count(20) + + y_scale = + Contex.ContinuousLinearScale.new() + |> Contex.ContinuousLinearScale.domain(0, max_value) + + chart = + case chart_type do + :line -> Contex.LinePlot + :scatter -> Contex.PointPlot + end + + options = [ + smoothed: false, + colour_palette: ["f8d98b"], + custom_x_scale: x_scale, + custom_y_scale: y_scale + ] + + data + |> Contex.Dataset.new() + |> Contex.Plot.new(chart, 800, 300, options) + |> Map.put(:margins, %{left: 60, right: 40, top: 20, bottom: 70}) + |> Contex.Plot.to_svg() + end + + defp time_scale_start(now, {"hour", amount}), do: NaiveDateTime.shift(now, hour: -amount) + defp time_scale_start(now, {"day", amount}), do: NaiveDateTime.shift(now, day: -amount) + + defp schedule_health_check_timer(socket) do + if connected?(socket) and device_health_check_enabled?() do + timer_ref = Process.send_after(self(), :check_health_interval, 500) + assign(socket, :health_check_timer, timer_ref) + else + assign(socket, :health_check_timer, nil) + end + end + + defp device_health_check_enabled?() do + Application.get_env(:nerves_hub, :device_health_check_enabled) + end +end diff --git a/lib/nerves_hub_web/live/devices/device_health.html.heex b/lib/nerves_hub_web/live/devices/device_health.html.heex new file mode 100644 index 000000000..f0ac781da --- /dev/null +++ b/lib/nerves_hub_web/live/devices/device_health.html.heex @@ -0,0 +1,50 @@ +
+ <.link navigate={~p"/org/#{@org.name}/#{@product.name}/devices/#{@device.identifier}"} class="back-link">Back to + device +
+ + +
+ +
+

Device Metrics

+ +
+ + +
+
+ <%= for {unit, amount} <- @time_frame_opts do %> + + <% end %> +
+
+ +
+
+ + + + + +
+
diff --git a/lib/nerves_hub_web/live/devices/show.html.heex b/lib/nerves_hub_web/live/devices/show.html.heex index 0eaf8f0a1..7f07fbee5 100644 --- a/lib/nerves_hub_web/live/devices/show.html.heex +++ b/lib/nerves_hub_web/live/devices/show.html.heex @@ -129,11 +129,18 @@ -
- Last reported : - +
+
+ Last reported : + +
+
+ <.link navigate={~p"/org/#{@org.name}/#{@product.name}/devices/#{@device.identifier}/health"}> + Full metrics + +
diff --git a/lib/nerves_hub_web/router.ex b/lib/nerves_hub_web/router.ex index 1a6b5cd65..3635ec62b 100644 --- a/lib/nerves_hub_web/router.ex +++ b/lib/nerves_hub_web/router.ex @@ -262,6 +262,11 @@ defmodule NervesHubWeb.Router do live("/org/:org_name/:product_name/devices/new", Live.Devices.New) live("/org/:org_name/:product_name/devices/:device_identifier", Live.Devices.Show) + live( + "/org/:org_name/:product_name/devices/:device_identifier/health", + Live.Devices.DeviceHealth + ) + live( "/org/:org_name/:product_name/devices/:device_identifier/settings", Live.Devices.Settings diff --git a/mix.exs b/mix.exs index bc4f358e3..2469e4944 100644 --- a/mix.exs +++ b/mix.exs @@ -65,6 +65,7 @@ defmodule NervesHub.MixProject do {:castore, "~> 1.0"}, {:circular_buffer, "~> 0.4.1"}, {:comeonin, "~> 5.3"}, + {:contex, "~> 0.5.0"}, {:crontab, "~> 1.1"}, {:decorator, "~> 1.2"}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index c11f092d9..f885ec8c3 100644 --- a/mix.lock +++ b/mix.lock @@ -8,6 +8,7 @@ "circular_buffer": {:hex, :circular_buffer, "0.4.1", "477f370fd8cfe1787b0a1bade6208bbd274b34f1610e41f1180ba756a7679839", [:mix], [], "hexpm", "633ef2e059dde0d7b89bbab13b1da9d04c6685e80e68fbdf41282d4fae746b72"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, + "contex": {:hex, :contex, "0.5.0", "5d8a6defbeb41f54adfcb0f85c4756d4f2b84aa5b0d809d45a5d2e90d91d0392", [:mix], [{:nimble_strftime, "~> 0.1.0", [hex: :nimble_strftime, repo: "hexpm", optional: false]}], "hexpm", "b7497a1790324d84247859df44ba4bcf2489d9bba1812a5375b2f2046b9e6fd7"}, "crontab": {:hex, :crontab, "1.1.13", "3bad04f050b9f7f1c237809e42223999c150656a6b2afbbfef597d56df2144c5", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "d67441bec989640e3afb94e123f45a2bc42d76e02988c9613885dc3d01cf7085"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, @@ -49,6 +50,7 @@ "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_ownership": {:hex, :nimble_ownership, "0.3.1", "99d5244672fafdfac89bfad3d3ab8f0d367603ce1dc4855f86a1c75008bce56f", [:mix], [], "hexpm", "4bf510adedff0449a1d6e200e43e57a814794c8b5b6439071274d248d272a549"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "nimble_strftime": {:hex, :nimble_strftime, "0.1.1", "b988184d1bd945bc139b2c27dd00a6c0774ec94f6b0b580083abd62d5d07818b", [:mix], [], "hexpm", "89e599c9b8b4d1203b7bb5c79eb51ef7c6a28fbc6228230b312f8b796310d755"}, "oban": {:hex, :oban, "2.17.12", "33fb0cbfb92b910d48dd91a908590fe3698bb85eacec8cd0d9bc6aa13dddd6d6", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7a647d6cd6bb300073db17faabce22d80ae135da3baf3180a064fa7c4fa046e3"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, diff --git a/test/nerves_hub/devices_test.exs b/test/nerves_hub/devices_test.exs index f602c4693..94d916f94 100644 --- a/test/nerves_hub/devices_test.exs +++ b/test/nerves_hub/devices_test.exs @@ -1047,12 +1047,12 @@ defmodule NervesHub.DevicesTest do assert {:ok, %Devices.DeviceHealth{}} = inserted end - healths = Devices.get_all_health(device.id) + healths = Devices.get_device_health(device.id) assert 10 = Enum.count(healths) Devices.truncate_device_health() - healths = Devices.get_all_health(device.id) + healths = Devices.get_device_health(device.id) assert 7 = Enum.count(healths) end end diff --git a/test/nerves_hub/workers/device_health_truncation_test.exs b/test/nerves_hub/workers/device_health_truncation_test.exs index e30c63a5c..160c43c7a 100644 --- a/test/nerves_hub/workers/device_health_truncation_test.exs +++ b/test/nerves_hub/workers/device_health_truncation_test.exs @@ -27,7 +27,7 @@ defmodule NervesHub.Workers.DeviceHealthTruncationTest do assert :ok = perform_job(DeviceHealthTruncation, %{}) - healths = Devices.get_all_health(device.id) + healths = Devices.get_device_health(device.id) assert 7 = Enum.count(healths) end end diff --git a/test/nerves_hub_web/live/devices/health_test.exs b/test/nerves_hub_web/live/devices/health_test.exs new file mode 100644 index 000000000..8604ce00b --- /dev/null +++ b/test/nerves_hub_web/live/devices/health_test.exs @@ -0,0 +1,110 @@ +defmodule NervesHubWeb.Devices.HealthTest do + use NervesHubWeb.ConnCase.Browser, async: false + + alias NervesHub.Devices + alias NervesHubWeb.Live.Devices.DeviceHealth + + alias NervesHubWeb.Endpoint + + setup %{fixture: %{device: device}} do + Endpoint.subscribe("device:#{device.id}") + end + + test "assert page render when no health exist for device", %{ + conn: conn, + org: org, + product: product, + device: device + } do + conn + |> visit("/org/#{org.name}/#{product.name}/devices/#{device.identifier}/health") + |> assert_has("h1", text: "Device Health") + |> assert_has(".metrics-text", text: "No data for selected period") + end + + test "Assert svg is rendered when metrics data exists", %{ + conn: conn, + org: org, + product: product, + device: device + } do + device_health = %{ + "device_id" => device.id, + "data" => %{ + "metrics" => %{ + "cpu_temp" => 41.381, + "load_15min" => 0.06, + "load_1min" => 0.55, + "load_5min" => 0.15, + "size_mb" => 7892, + "used_mb" => 172, + "used_percent" => 2 + }, + "timestamp" => "2024-08-26T15:44:18.295149Z" + } + } + + assert {:ok, %Devices.DeviceHealth{}} = Devices.save_device_health(device_health) + + conn + |> visit("/org/#{org.name}/#{product.name}/devices/#{device.identifier}/health") + |> assert_has("svg") + end + + test "assert health data without metrics doesn't crash liveview", %{ + conn: conn, + org: org, + product: product, + device: device + } do + device_health = %{ + "device_id" => device.id, + "data" => %{ + "timestamp" => "2024-08-26T15:44:18.295149Z" + } + } + + assert {:ok, %Devices.DeviceHealth{}} = Devices.save_device_health(device_health) + + conn + |> visit("/org/#{org.name}/#{product.name}/devices/#{device.identifier}/health") + |> assert_has(".metrics-text", text: "No data for selected period") + end + + test "assert metrics with nil data is rejected", %{ + device: device + } do + valid_data = %{ + "device_id" => device.id, + "data" => %{ + "metrics" => %{ + "cpu_temp" => 41.381, + "load_15min" => 0.06, + "load_1min" => 0.55, + "load_5min" => 0.15, + "size_mb" => 7892, + "used_mb" => 172, + "used_percent" => 2 + }, + "timestamp" => "2024-08-26T15:44:18.295149Z" + } + } + + invalid_data = %{ + "device_id" => device.id, + "data" => %{ + "timestamp" => "2024-08-26T15:44:18.295149Z" + } + } + + assert {:ok, %Devices.DeviceHealth{}} = Devices.save_device_health(valid_data) + assert {:ok, %Devices.DeviceHealth{}} = Devices.save_device_health(invalid_data) + + metrics = + device.id + |> Devices.get_device_health() + |> DeviceHealth.organize_data() + + assert length(metrics.cpu_temp) == 1 + end +end