Skip to content

Commit

Permalink
Ensure available deployments match selected devices architecture and …
Browse files Browse the repository at this point in the history
…platform when batch-updating
  • Loading branch information
nshoes committed Feb 20, 2025
1 parent 6b93041 commit 75ab470
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 86 deletions.
4 changes: 2 additions & 2 deletions lib/nerves_hub_web/live/devices/index-new.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,9 @@
<label for="move_to" class="sidebar-label">Move device(s) to deployment:</label>

<div class="flex gap-2">
<select name="deployment" id="move_to_deployment" class="sidebar-select">
<select name="deployment" id="move_to_deployment" class="sidebar-select" disabled={Enum.empty?(@available_deployments_for_selected_devices)}>
<option value="">Select deployment</option>
<%= for deployment <- @deployments do %>
<%= for deployment <- @available_deployments_for_selected_devices do %>
<option value={deployment.id} {if @target_deployment && @target_deployment.id == deployment.id, do: [selected: true], else: []}>
{deployment.name} - {deployment.firmware.architecture} - {deployment.firmware.platform}
</option>
Expand Down
96 changes: 89 additions & 7 deletions lib/nerves_hub_web/live/devices/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ defmodule NervesHubWeb.Live.Devices.Index do
use NervesHubWeb, :updated_live_view

require Logger

require OpenTelemetry.Tracer, as: Tracer

import Ecto.Query

alias NervesHub.AuditLogs.DeviceTemplates
alias NervesHub.Deployments
alias NervesHub.Deployments.Deployment
alias NervesHub.Devices
alias NervesHub.Devices.Alarms
alias NervesHub.Devices.Device
alias NervesHub.Devices.Metrics
alias NervesHub.Firmwares
alias NervesHub.Products.Product
alias NervesHub.Repo
alias NervesHub.Tracker

alias Phoenix.LiveView.JS
Expand Down Expand Up @@ -98,7 +101,7 @@ defmodule NervesHubWeb.Live.Devices.Index do
|> assign(:total_entries, 0)
|> assign(:current_alarms, Alarms.get_current_alarm_types(product.id))
|> assign(:metrics_keys, Metrics.default_metrics())
|> assign(:deployments, Deployments.get_deployments_by_product(product))
|> assign(:available_deployments_for_selected_devices, [])
|> assign(:target_deployment, nil)
|> subscribe_and_refresh_device_list_timer()
|> ok()
Expand Down Expand Up @@ -222,7 +225,12 @@ defmodule NervesHubWeb.Live.Devices.Index do
[id | selected_devices]
end

{:noreply, assign(socket, :selected_devices, selected_devices)}
socket =
socket
|> assign(:selected_devices, selected_devices)
|> maybe_update_available_deployments_for_selected_devices()

{:noreply, socket}
end

def handle_event("select-all", _, socket) do
Expand All @@ -235,11 +243,17 @@ defmodule NervesHubWeb.Live.Devices.Index do
Enum.map(socket.assigns.devices, & &1.id)
end

{:noreply, assign(socket, :selected_devices, selected_devices)}
socket =
socket
|> assign(:selected_devices, selected_devices)
|> maybe_update_available_deployments_for_selected_devices()

{:noreply, socket}
end

def handle_event("deselect-all", _, socket) do
{:noreply, assign(socket, selected_devices: [])}
{:noreply,
assign(socket, %{selected_devices: [], available_deployments_for_selected_devices: []})}
end

def handle_event("validate-tags", %{"tags" => tags}, socket) do
Expand Down Expand Up @@ -289,7 +303,10 @@ defmodule NervesHubWeb.Live.Devices.Index do

def handle_event("target-deployment", %{"deployment" => deployment_id}, socket) do
deployment =
Enum.find(socket.assigns.deployments, &(&1.id == String.to_integer(deployment_id)))
Enum.find(
socket.assigns.available_deployments_for_selected_devices,
&(&1.id == String.to_integer(deployment_id))
)

{:noreply, assign(socket, target_deployment: deployment)}
end
Expand Down Expand Up @@ -702,4 +719,69 @@ defmodule NervesHubWeb.Live.Devices.Index do
background-size: #{progress}% 1px, #{progress * 1.1}% 100%;
"""
end

# if selected devices have matching architecture and platform, find available deployments
defp maybe_update_available_deployments_for_selected_devices(
%{
assigns: %{
product: %{id: product_id},
selected_devices: selected_devices
}
} = socket
) do
selected_devices_arch_and_platform_match? =
selected_devices_arch_and_platform_match?(selected_devices)

if selected_devices_arch_and_platform_match? do
deployments =
get_deployments_by_product_architecture_platform(product_id, selected_devices)

socket
|> assign(:available_deployments_for_selected_devices, deployments)
|> assign(:debounce_active, true)
else
assign(socket, :available_deployments_for_selected_devices, [])
end
end

defp selected_devices_arch_and_platform_match?(device_ids) do
from(d in Device,
where: d.id in ^device_ids,
select: [
fragment("firmware_metadata ->> 'architecture'"),
fragment("firmware_metadata ->> 'platform'")
],
distinct: true
)
|> Repo.all()
|> then(&(length(&1) == 1))
end

# defp get_deployments_by_product_architecture_platform(_product_id, []), do: []

defp get_deployments_by_product_architecture_platform(product_id, device_ids) do
unique_architectures_from_selected_devices_query =
from(d in Device,
where: d.id in ^device_ids,
distinct: fragment("firmware_metadata ->> 'architecture'"),
select: fragment("firmware_metadata ->> 'architecture'")
)

unique_platforms_from_selected_devices_query =
from(d in Device,
where: d.id in ^device_ids,
distinct: fragment("firmware_metadata ->> 'platform'"),
select: fragment("firmware_metadata ->> 'platform'")
)

from(d in Deployment,
join: f in assoc(d, :firmware),
where: d.product_id == ^product_id,
where: f.architecture in subquery(unique_architectures_from_selected_devices_query),
where: f.platform in subquery(unique_platforms_from_selected_devices_query),
distinct: true,
preload: [firmware: f]
)
|> Repo.all()
end
end
4 changes: 2 additions & 2 deletions lib/nerves_hub_web/live/devices/index.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,9 @@
<label for="move_to_deployment">Move device(s) to deployment:</label>
<div class="flex-row align-items-center">
<div class="flex-grow pos-rel">
<select name="deployment" id="move_to_deployment" class="form-control">
<select name="deployment" id="move_to_deployment" class="form-control" disabled={Enum.empty?(@available_deployments_for_selected_devices)}>
<option value=""></option>
<%= for deployment <- @deployments do %>
<%= for deployment <- @available_deployments_for_selected_devices do %>
<option value={deployment.id} {if @target_deployment && @target_deployment.id == deployment.id, do: [selected: true], else: []}>{deployment.name}</option>
<% end %>
</select>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule NervesHub.Repo.Migrations.CreateDevicesFirmwareMetadataArchitectureIndex do
use Ecto.Migration
@disable_ddl_transaction true


def change do
create_if_not_exists index("devices", ["(firmware_metadata->'architecture')"], name: :devices_architecture_index, using: "GIN", concurrently: true)
end
end
75 changes: 0 additions & 75 deletions test/nerves_hub_web/live/devices/index_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ defmodule NervesHubWeb.Live.Devices.IndexTest do
use NervesHubWeb.ConnCase.Browser, async: false

alias NervesHub.Devices
alias NervesHub.Firmwares.FirmwareMetadata
alias NervesHub.Fixtures

alias NervesHub.Repo
Expand Down Expand Up @@ -317,80 +316,6 @@ defmodule NervesHubWeb.Live.Devices.IndexTest do
assert Repo.reload(device) |> Map.get(:deployment_id)
assert Repo.reload(device2) |> Map.get(:deployment_id)
end

test "selecting multiple devices to add to deployment but some don't match firmware requirements",
%{conn: conn, fixture: fixture} do
%{
device: device,
org: org,
product: product,
firmware: firmware,
deployment: deployment
} = fixture

device2 = Fixtures.device_fixture(org, product, firmware)

different_firmware_params =
%FirmwareMetadata{device2.firmware_metadata | platform: "foo"} |> Map.from_struct()

{:ok, device2} = Devices.update_firmware_metadata(device2, different_firmware_params)

refute device.deployment_id
refute device2.deployment_id

conn
|> visit("/org/#{org.name}/#{product.name}/devices")
|> unwrap(fn view ->
render_change(view, "select-all", %{"id" => device.id})
end)
|> assert_has("span", text: "2 selected")
|> unwrap(fn view ->
render_change(view, "target-deployment", %{"deployment" => to_string(deployment.id)})
end)
|> click_button("#move-deployment-submit", "Move")
|> assert_has("div", text: "1 device added to deployment")
|> assert_has("div", text: "1 device could not be added")

assert Repo.reload(device) |> Map.get(:deployment_id)
refute Repo.reload(device2) |> Map.get(:deployment_id)
end

test "selecting multiple devices to add to deployment but none match firmware requirements",
%{conn: conn, fixture: fixture} do
%{
device: device,
org: org,
product: product,
firmware: firmware,
deployment: deployment
} = fixture

device2 = Fixtures.device_fixture(org, product, firmware)

different_firmware_params =
%FirmwareMetadata{device2.firmware_metadata | platform: "foo"} |> Map.from_struct()

{:ok, device} = Devices.update_firmware_metadata(device, different_firmware_params)
{:ok, device2} = Devices.update_firmware_metadata(device2, different_firmware_params)

refute device.deployment_id
refute device2.deployment_id

conn
|> visit("/org/#{org.name}/#{product.name}/devices")
|> unwrap(fn view ->
render_change(view, "select-all", %{"id" => device.id})
end)
|> assert_has("span", text: "2 selected")
|> unwrap(fn view ->
render_change(view, "target-deployment", %{"deployment" => to_string(deployment.id)})
end)
|> click_button("#move-deployment-submit", "Move")
|> assert_has("div", text: "No devices selected could be added to deployment")

refute Repo.reload(device) |> Map.get(:deployment_id)
refute Repo.reload(device2) |> Map.get(:deployment_id)
end
end

def device_index_path(%{org: org, product: product}) do
Expand Down

0 comments on commit 75ab470

Please sign in to comment.