diff --git a/lib/towerops/gaiia/inventory_creator.ex b/lib/towerops/gaiia/inventory_creator.ex index 47971958..609e19b2 100644 --- a/lib/towerops/gaiia/inventory_creator.ex +++ b/lib/towerops/gaiia/inventory_creator.ex @@ -50,30 +50,31 @@ defmodule Towerops.Gaiia.InventoryCreator do end @doc """ - Find the best-matching Gaiia inventory model for a device. + List all manufacturers from Gaiia's inventory model library. - Queries Gaiia's inventory model library, filtering by the device's - SNMP-discovered model name. Returns `{:ok, model_id, model_name}` or - `{:error, reason}`. + Returns `{:ok, [%{"name" => name, "id" => id}]}` or `{:error, reason}`. """ - def find_matching_model(organization_id, %Device{} = device) do - snmp = Repo.get_by(SnmpDevice, device_id: device.id) + def list_manufacturers(organization_id) do + with {:ok, client} <- build_client(organization_id) do + fetch_manufacturers(client) + end + end - with true <- is_binary(snmp && snmp.manufacturer) and is_binary(snmp && snmp.model), - {:ok, client} <- build_client(organization_id), - {:ok, models} <- fetch_models(client, snmp.model), - {:ok, model} <- pick_best_match(models, snmp.manufacturer) do - {:ok, model["id"], model["name"]} - else - false -> {:error, :no_snmp_model_info} - {:error, _} = error -> error + @doc """ + List inventory models for a specific manufacturer. + + Returns `{:ok, [%{"id" => id, "name" => name}]}` or `{:error, reason}`. + """ + def list_models(organization_id, manufacturer_id) do + with {:ok, client} <- build_client(organization_id) do + fetch_models_for_manufacturer(client, manufacturer_id) end end @doc """ Create a Gaiia inventory item for a device. - Takes a matched model ID, device, and optional Gaiia network site ID. + Takes a model ID, device, and optional Gaiia network site ID. Uses `startAddInventoryItemsJob` to create the item. Returns `{:ok, job_id}` or `{:error, reason}`. @@ -91,41 +92,46 @@ defmodule Towerops.Gaiia.InventoryCreator do Application.get_env(:towerops, :gaiia_api_endpoint, "https://api.gaiia.com/api/v1") end - defp fetch_models(client, search_name) do - filters = %{"name" => %{"contains" => search_name}} - + defp fetch_manufacturers(client) do case Gaiia.Queries.inventory_models( client, - %{"filters" => filters, "first" => 10}, - "edges { node { id name manufacturer { name } } }" + %{"first" => 200}, + "edges { node { manufacturer { id name } } }" ) do {:ok, %{"edges" => edges}} when is_list(edges) -> - {:ok, Enum.map(edges, & &1["node"])} + manufacturers = + edges + |> Enum.map(fn e -> get_in(e, ["node", "manufacturer"]) end) + |> Enum.reject(&is_nil/1) + |> Enum.uniq_by(& &1["id"]) + |> Enum.sort_by(& &1["name"]) - {:ok, _} -> - {:error, :unexpected_response} + {:ok, manufacturers} {:error, reason} -> - Logger.error("Failed to query Gaiia inventory models: #{inspect(reason)}") + Logger.error("Failed to fetch Gaiia manufacturers: #{inspect(reason)}") {:error, :gaiia_api_error} end end - defp pick_best_match([], _manufacturer), do: {:error, :no_matching_model} + defp fetch_models_for_manufacturer(client, manufacturer_id) do + case Gaiia.Queries.inventory_models( + client, + %{"first" => 200}, + "edges { node { id name manufacturer { id } } }" + ) do + {:ok, %{"edges" => edges}} when is_list(edges) -> + models = + edges + |> Enum.map(& &1["node"]) + |> Enum.filter(fn m -> get_in(m, ["manufacturer", "id"]) == manufacturer_id end) + |> Enum.sort_by(& &1["name"]) - defp pick_best_match(models, manufacturer) do - manufacturer_down = String.downcase(manufacturer) + {:ok, models} - exact = - Enum.filter(models, fn m -> - mf = get_in(m, ["manufacturer", "name"]) || "" - String.downcase(mf) == manufacturer_down - end) - - if exact == [] do - {:ok, hd(models)} - else - {:ok, hd(exact)} + {:error, reason} -> + Logger.error("Failed to fetch Gaiia models: #{inspect(reason)}") + {:error, :gaiia_api_error} end end diff --git a/lib/towerops_web/live/org/gaiia_reconciliation_live.ex b/lib/towerops_web/live/org/gaiia_reconciliation_live.ex index c346faae..44e3e353 100644 --- a/lib/towerops_web/live/org/gaiia_reconciliation_live.ex +++ b/lib/towerops_web/live/org/gaiia_reconciliation_live.ex @@ -13,7 +13,11 @@ defmodule ToweropsWeb.Org.GaiiaReconciliationLive do {:ok, socket |> assign(:organization, org) - |> assign(:page_title, t("Gaiia Inventory Reconciliation"))} + |> assign(:page_title, t("Gaiia Inventory Reconciliation")) + |> assign(:open_create_for, nil) + |> assign(:manufacturers, []) + |> assign(:models, %{}) + |> assign(:selected_manufacturer, nil)} end @impl true @@ -36,18 +40,67 @@ defmodule ToweropsWeb.Org.GaiiaReconciliationLive do |> load_reconciliation()} end - def handle_event("create_gaiia_item", %{"device_id" => device_id}, socket) do + def handle_event("open_create_form", %{"device_id" => device_id}, socket) do org_id = socket.assigns.organization.id - result = - with {:ok, device} <- get_device(device_id), - {:ok, model_id, model_name} <- InventoryCreator.find_matching_model(org_id, device), - {:ok, _job_id} <- InventoryCreator.create_inventory_item(org_id, device, model_id) do - {:ok, device.name, model_name} + socket = + if socket.assigns.manufacturers == [] do + case InventoryCreator.list_manufacturers(org_id) do + {:ok, manufacturers} -> assign(socket, :manufacturers, manufacturers) + {:error, _} -> socket + end + else + socket end {:noreply, socket + |> assign(:open_create_for, device_id) + |> assign(:selected_manufacturer, nil)} + end + + def handle_event("cancel_create", _params, socket) do + {:noreply, + socket + |> assign(:open_create_for, nil) + |> assign(:selected_manufacturer, nil)} + end + + def handle_event("select_manufacturer", %{"manufacturer_id" => manufacturer_id}, socket) do + org_id = socket.assigns.organization.id + existing = socket.assigns.models + + socket = + if Map.has_key?(existing, manufacturer_id) do + assign(socket, :selected_manufacturer, manufacturer_id) + else + case InventoryCreator.list_models(org_id, manufacturer_id) do + {:ok, models} -> + socket + |> assign(:models, Map.put(existing, manufacturer_id, models)) + |> assign(:selected_manufacturer, manufacturer_id) + + {:error, _} -> + socket + end + end + + {:noreply, socket} + end + + def handle_event("create_gaiia_item", %{"device_id" => device_id, "model_id" => model_id}, socket) do + org_id = socket.assigns.organization.id + + result = + with {:ok, device} <- get_device(device_id), + {:ok, _job_id} <- InventoryCreator.create_inventory_item(org_id, device, model_id) do + {:ok, device.name} + end + + {:noreply, + socket + |> assign(:open_create_for, nil) + |> assign(:selected_manufacturer, nil) |> put_flash(:info, create_flash(result)) |> load_reconciliation()} end @@ -86,19 +139,8 @@ defmodule ToweropsWeb.Org.GaiiaReconciliationLive do end end - defp create_flash({:ok, device_name, model_name}) do - t("Created Gaiia inventory item for %{device} (model: %{model}).", - device: device_name, - model: model_name - ) - end - - defp create_flash({:error, :no_snmp_model_info}) do - t("Device has no SNMP manufacturer/model data. Run SNMP discovery first.") - end - - defp create_flash({:error, :no_matching_model}) do - t("No matching Gaiia inventory model found for this device.") + defp create_flash({:ok, device_name}) do + t("Created Gaiia inventory item for %{device}.", device: device_name) end defp create_flash({:error, :no_gaiia_integration}) do diff --git a/lib/towerops_web/live/org/gaiia_reconciliation_live.html.heex b/lib/towerops_web/live/org/gaiia_reconciliation_live.html.heex index c4ab9e7c..7759a687 100644 --- a/lib/towerops_web/live/org/gaiia_reconciliation_live.html.heex +++ b/lib/towerops_web/live/org/gaiia_reconciliation_live.html.heex @@ -183,13 +183,59 @@ <% end %> - + <%= if @open_create_for == entry.device.id do %> +
+ + <%= if @selected_manufacturer do %> +
+ + + +
+ <% end %> + +
+ <% else %> + + <% end %> <% end %>