refactor(gaiia): manufacturer/model dropdowns for inventory creation
Replaces auto-matching with explicit two-step selection: 1. Select manufacturer from Gaiia's inventory model library (dropdown) 2. Select model for that manufacturer (dropdown, filtered live) 3. Click Create Manufacturers and models are fetched from Gaiia on demand and cached in socket assigns. Only one device form is open at a time.
This commit is contained in:
parent
31231d9fad
commit
2695e8a0eb
3 changed files with 158 additions and 64 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -183,13 +183,59 @@
|
|||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-right text-sm">
|
||||
<button
|
||||
phx-click="create_gaiia_item"
|
||||
phx-value-device_id={entry.device.id}
|
||||
class="inline-flex items-center gap-1 rounded bg-indigo-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
{t("Create in Gaiia")}
|
||||
</button>
|
||||
<%= if @open_create_for == entry.device.id do %>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<select
|
||||
phx-change="select_manufacturer"
|
||||
name="manufacturer_id"
|
||||
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<option value="">{t("Select manufacturer")}</option>
|
||||
<%= for mfr <- @manufacturers do %>
|
||||
<option
|
||||
value={mfr["id"]}
|
||||
selected={@selected_manufacturer == mfr["id"]}
|
||||
>
|
||||
{mfr["name"]}
|
||||
</option>
|
||||
<% end %>
|
||||
</select>
|
||||
<%= if @selected_manufacturer do %>
|
||||
<form
|
||||
phx-submit="create_gaiia_item"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<input type="hidden" name="device_id" value={entry.device.id} />
|
||||
<select
|
||||
name="model_id"
|
||||
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<option value="">{t("Select model")}</option>
|
||||
<%= for model <- Map.get(@models, @selected_manufacturer, []) do %>
|
||||
<option value={model["id"]}>{model["name"]}</option>
|
||||
<% end %>
|
||||
</select>
|
||||
<button class="rounded bg-indigo-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-indigo-700">
|
||||
{t("Create")}
|
||||
</button>
|
||||
</form>
|
||||
<% end %>
|
||||
<button
|
||||
phx-click="cancel_create"
|
||||
class="rounded px-2 py-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
{t("Cancel")}
|
||||
</button>
|
||||
</div>
|
||||
<% else %>
|
||||
<button
|
||||
phx-click="open_create_form"
|
||||
phx-value-device_id={entry.device.id}
|
||||
class="inline-flex items-center gap-1 rounded bg-indigo-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
{t("Create in Gaiia")}
|
||||
</button>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue