diff --git a/lib/nerves_hub/devices.ex b/lib/nerves_hub/devices.ex index d693d38af..5a84d2417 100644 --- a/lib/nerves_hub/devices.ex +++ b/lib/nerves_hub/devices.ex @@ -1156,6 +1156,21 @@ defmodule NervesHub.Devices do update_device_with_audit(device, params, user, description) end + @spec move_many_to_deployment([integer()], integer()) :: {integer(), nil} + def move_many_to_deployment(device_ids, deployment_id) do + firmware = + Firmware + |> join(:inner, [f], d in Deployment, on: d.id == ^deployment_id) + |> limit(1) + |> Repo.one() + + Device + |> where([d], d.id in ^device_ids) + |> where([d], d.firmware_metadata["platform"] == ^firmware.platform) + |> where([d], d.firmware_metadata["architecture"] == ^firmware.architecture) + |> Repo.update_all(set: [deployment_id: deployment_id]) + end + @spec move_many([Device.t()], Product.t(), User.t()) :: %{ ok: [Device.t()], error: [{Ecto.Multi.name(), any()}] diff --git a/lib/nerves_hub/products/product.ex b/lib/nerves_hub/products/product.ex index 8cc649698..3a2c38ee1 100644 --- a/lib/nerves_hub/products/product.ex +++ b/lib/nerves_hub/products/product.ex @@ -4,6 +4,7 @@ defmodule NervesHub.Products.Product do alias NervesHub.Accounts.Org alias NervesHub.Archives.Archive + alias NervesHub.Deployments.Deployment alias NervesHub.Scripts.Script alias NervesHub.Devices.CACertificate alias NervesHub.Devices.Device @@ -22,6 +23,7 @@ defmodule NervesHub.Products.Product do has_many(:jitp, CACertificate.JITP) has_many(:archives, Archive) has_many(:scripts, Script) + has_many(:deployments, Deployment) has_many(:shared_secret_auths, SharedSecretAuth, preload_order: [desc: :deactivated_at, asc: :id] diff --git a/lib/nerves_hub_web/live/devices/index.ex b/lib/nerves_hub_web/live/devices/index.ex index 4557503d2..9ae9318ad 100644 --- a/lib/nerves_hub_web/live/devices/index.ex +++ b/lib/nerves_hub_web/live/devices/index.ex @@ -12,6 +12,7 @@ defmodule NervesHubWeb.Live.Devices.Index do alias NervesHub.Firmwares alias NervesHub.Products.Product alias NervesHub.Tracker + alias NervesHub.Repo alias Phoenix.Socket.Broadcast alias Phoenix.LiveView.JS @@ -73,8 +74,8 @@ defmodule NervesHubWeb.Live.Devices.Index do total_pages: :integer } - def mount(_params, _session, socket) do - %{product: product} = socket.assigns + def mount(_params, _session, %{assigns: %{product: product}} = socket) do + product = Repo.preload(product, :deployments) socket |> page_title("Devices - #{product.name}") @@ -93,6 +94,8 @@ 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, product.deployments) + |> assign(:target_deployment, nil) |> subscribe_and_refresh_device_list_timer() |> ok() end @@ -272,7 +275,18 @@ defmodule NervesHubWeb.Live.Devices.Index do {:noreply, assign(socket, target_product: target)} end - def handle_event("move-devices", _, socket) do + def handle_event("target-deployment", %{"deployment" => ""}, socket) do + {:noreply, assign(socket, target_deployment: nil)} + end + + def handle_event("target-deployment", %{"deployment" => deployment_id}, socket) do + deployment = + Enum.find(socket.assigns.deployments, &(&1.id == String.to_integer(deployment_id))) + + {:noreply, assign(socket, target_deployment: deployment)} + end + + def handle_event("move-devices-product", _, socket) do %{ok: successfuls} = Devices.get_devices_by_id(socket.assigns.selected_devices) |> Devices.move_many(socket.assigns.target_product, socket.assigns.user) @@ -289,6 +303,60 @@ defmodule NervesHubWeb.Live.Devices.Index do {:noreply, socket} end + def handle_event( + "move-devices-deployment", + _, + %{ + assigns: %{ + selected_devices: selected_devices, + target_deployment: target_deployment + } + } = socket + ) do + {devices_updated_count, _} = + Devices.move_many_to_deployment(selected_devices, target_deployment.id) + + devices_not_updated_count = length(selected_devices) - devices_updated_count + + maybe_pluralize = + &if &1 == 1 do + &2 + else + &2 <> "s" + end + + socket = + socket + |> assign(:target_deployment, nil) + |> assign_display_devices() + |> then(fn socket -> + case [devices_updated_count, devices_not_updated_count] do + [updated_count, 0] -> + put_flash( + socket, + :info, + "#{updated_count} #{maybe_pluralize.(updated_count, "device")} added to deployment #{target_deployment.name}" + ) + + [0, _not_updated_count] -> + put_flash( + socket, + :info, + "No devices selected could be added to deployment #{target_deployment.name} because of mismatched firmware" + ) + + [updated_count, not_updated_count] -> + put_flash( + socket, + :info, + "#{updated_count} #{maybe_pluralize.(updated_count, "device")} added to deployment #{target_deployment.name}. #{not_updated_count} #{maybe_pluralize.(not_updated_count, "device")} could not be added to deployment because of mismatched firmware" + ) + end + end) + + {:noreply, socket} + end + def handle_event("disable-updates-for-devices", _, socket) do %{ok: _successfuls} = Devices.get_devices_by_id(socket.assigns.selected_devices) diff --git a/lib/nerves_hub_web/live/devices/index.html.heex b/lib/nerves_hub_web/live/devices/index.html.heex index 3f707c2b5..81d87e67f 100644 --- a/lib/nerves_hub_web/live/devices/index.html.heex +++ b/lib/nerves_hub_web/live/devices/index.html.heex @@ -193,8 +193,8 @@
-
- + +
+ + <%= for deployment <- @deployments do %> + + <% end %> + +
+
+ + +
+
diff --git a/test/nerves_hub_web/live/devices/index_test.exs b/test/nerves_hub_web/live/devices/index_test.exs index cf3bead62..ca460cf1d 100644 --- a/test/nerves_hub_web/live/devices/index_test.exs +++ b/test/nerves_hub_web/live/devices/index_test.exs @@ -2,8 +2,10 @@ defmodule NervesHubWeb.Live.Devices.IndexTest do use NervesHubWeb.ConnCase.Browser, async: false alias NervesHub.Devices + alias NervesHub.Firmwares.FirmwareMetadata alias NervesHub.Fixtures alias NervesHubWeb.Endpoint + alias NervesHub.Repo setup %{fixture: %{device: device}} do Endpoint.subscribe("device:#{device.id}") @@ -278,6 +280,111 @@ defmodule NervesHubWeb.Live.Devices.IndexTest do |> click_button("Set") |> assert_has("span", text: "moussaka") end + + test "add multiple devices to deployment", + %{conn: conn, fixture: fixture} do + %{ + device: device, + org: org, + product: product, + firmware: firmware, + deployment: deployment + } = fixture + + device2 = Fixtures.device_fixture(org, product, firmware) + + 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: "2 devices added to deployment") + + 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