diff --git a/lib/towerops_web/live/org/integrations_live.ex b/lib/towerops_web/live/org/integrations_live.ex index 43990566..25e56698 100644 --- a/lib/towerops_web/live/org/integrations_live.ex +++ b/lib/towerops_web/live/org/integrations_live.ex @@ -293,6 +293,13 @@ defmodule ToweropsWeb.Org.IntegrationsLive do end) end + defp find_category_id(provider_id) do + case find_category(provider_id) do + %{id: id} -> id + nil -> nil + end + end + defp exclusive_conflict?(provider_id, integrations) do case find_category(provider_id) do %{exclusive: true} = category -> diff --git a/lib/towerops_web/live/org/integrations_live.html.heex b/lib/towerops_web/live/org/integrations_live.html.heex index 3710b17b..9c61fbf8 100644 --- a/lib/towerops_web/live/org/integrations_live.html.heex +++ b/lib/towerops_web/live/org/integrations_live.html.heex @@ -27,45 +27,35 @@

{category.description} - <%= if category.exclusive do %> - - Choose one - - <% end %>

-
-
-
-
-
- <.icon name={provider.icon} class="h-6 w-6 text-indigo-600 dark:text-indigo-400" /> -
-
-

- {provider.name} -

-

- {provider.description} -

- - <%= if integration = @integrations[provider.id] do %> -
- - {if integration.enabled, do: "Enabled", else: "Disabled"} + <%= if category.exclusive do %> + <%!-- Exclusive category (billing): show as picker + single config panel --%> + <% active_billing = Enum.find(category.providers, fn p -> @integrations[p.id] && @integrations[p.id].enabled end) %> +
+ <%= if active_billing do %> + <%!-- Active billing provider --%> + <% integration = @integrations[active_billing.id] %> +
+
+
+ <.icon name={active_billing.icon} class="h-6 w-6 text-indigo-600 dark:text-indigo-400" /> +
+
+
+

+ {active_billing.name} +

+ + Connected +
+

+ {active_billing.description} +

+
<%= if integration.last_synced_at do %> {t("Last synced:")} @@ -81,17 +71,10 @@ - "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" - - "partial" -> - "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" - - "failed" -> - "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" - - _ -> - "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400" + "success" -> "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" + "partial" -> "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" + "failed" -> "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" + _ -> "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400" end ]}> {String.capitalize(integration.last_sync_status)} @@ -100,104 +83,95 @@ <%= if integration.last_synced_at do %> - · Next sync in ~{next_sync_minutes( - integration.last_synced_at, - integration.sync_interval_minutes - )}m + · Next sync in ~{next_sync_minutes(integration.last_synced_at, integration.sync_interval_minutes)}m <% end %> - <%= if provider.id == "preseem" do %> + <%= if active_billing.id == "gaiia" do %> <.link - navigate={ - ~p"/orgs/#{@organization.slug}/settings/integrations/preseem/devices" - } - class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" - > - Manage Devices → - - <.link - navigate={~p"/insights?source=preseem"} - class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" - > - Network Insights → - - <% end %> - <%= if provider.id == "gaiia" do %> - <.link - navigate={ - ~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping" - } + navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"} class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" > Entity Mapping → <.link - navigate={ - ~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation" - } + navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"} class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" > Reconciliation → <% end %> - <%!-- Sync schedule info --%> - <%= if integration.enabled do %> -
-

- <.icon name="hero-clock" class="h-3.5 w-3.5 inline" /> Sync Schedule -

-

- <%= if provider.id == "preseem" do %> - Syncs every {integration.sync_interval_minutes} minutes. Baselines and fleet profiles computed nightly. - <% end %> - <%= if provider.id == "gaiia" do %> - Syncs every {integration.sync_interval_minutes} minutes. Reconciliation runs nightly. Also receives real-time webhook updates. - <% end %> -

-
- <% end %>
- <% end %> +
-
-
- <%= if integration = @integrations[provider.id] do %> +
- <% end %> + +
+
+ <%!-- Switch provider link --%> +
+

+ Using {active_billing.name}. To switch providers, disable this integration first. +

+
+ <% else %> + <%!-- No active billing — show provider picker --%> +

+ {t("Select your billing platform to get started:")} +

+
-
+ <% end %> - <%= if @configuring == provider.id do %> + <%!-- Config form for whichever billing provider is being configured --%> + <%= if @configuring && find_category_id(@configuring) == "billing" do %> + <% provider = Enum.find(category.providers, & &1.id == @configuring) %>
+

+ Configure {provider.name} +

<.form for={@form} id={"#{provider.id}-form"} @@ -256,7 +230,6 @@ > {t("Test Connection")} -
- <% end %> - <%= if provider.id == "gaiia" && @integrations["gaiia"] do %> -
-

- {t("Webhook Configuration")} -

-

- {t( - "Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change." - )} -

+ <%!-- Gaiia webhook config --%> + <%= if @configuring == "gaiia" && @integrations["gaiia"] do %> +
+

+ {t("Webhook Configuration")} +

+

+ {t("Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change.")} +

-
-
- -
- - +
+
+ +
+ + +
-
-
- -
- - +
+ +
+ + +
+
+ +
-
- -
-
-
-
- {t("Setup Instructions")} -
-
    -
  1. In your Gaiia admin panel, go to Settings → Webhooks
  2. -
  3. Click "Add Webhook"
  4. -
  5. Paste the Webhook URL above
  6. -
  7. Paste the Webhook Secret above into the "Secret" field
  8. -
  9. Select events: Account, Billing Subscription, Inventory Item
  10. -
  11. Save the webhook
  12. -
-

- {t( - "Once configured, Towerops will receive real-time updates when accounts, subscriptions, or inventory items change in Gaiia." - )} -

+
+
+ {t("Setup Instructions")} +
+
    +
  1. In your Gaiia admin panel, go to Settings → Webhooks
  2. +
  3. Click "Add Webhook"
  4. +
  5. Paste the Webhook URL above
  6. +
  7. Paste the Webhook Secret above into the "Secret" field
  8. +
  9. Select events: Account, Billing Subscription, Inventory Item
  10. +
  11. Save the webhook
  12. +
+
-
+ <% end %> <% end %>
-
+ <% else %> + <%!-- Non-exclusive categories: one card per provider --%> +
+
+
+
+
+ <.icon name={provider.icon} class="h-6 w-6 text-indigo-600 dark:text-indigo-400" /> +
+
+

+ {provider.name} +

+

+ {provider.description} +

+ + <%= if integration = @integrations[provider.id] do %> +
+ + {if integration.enabled, do: "Enabled", else: "Disabled"} + + + <%= if integration.last_synced_at do %> + + {t("Last synced:")} + <.timestamp + datetime={integration.last_synced_at} + timezone={@timezone} + format="absolute" + /> + + <% end %> + + <%= if integration.last_sync_status && integration.last_sync_status != "never" do %> + "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" + "partial" -> "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" + "failed" -> "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" + _ -> "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400" + end + ]}> + {String.capitalize(integration.last_sync_status)} + + <% end %> + + <%= if integration.last_synced_at do %> + + · Next sync in ~{next_sync_minutes(integration.last_synced_at, integration.sync_interval_minutes)}m + + <% end %> + + <%= if provider.id == "preseem" do %> + <.link + navigate={~p"/orgs/#{@organization.slug}/settings/integrations/preseem/devices"} + class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" + > + Manage Devices → + + <.link + navigate={~p"/insights?source=preseem"} + class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" + > + Network Insights → + + <% end %> + + <%= if integration.enabled do %> +
+

+ <.icon name="hero-clock" class="h-3.5 w-3.5 inline" /> Sync Schedule +

+

+ <%= if provider.id == "preseem" do %> + Syncs every {integration.sync_interval_minutes} minutes. Baselines and fleet profiles computed nightly. + <% end %> +

+
+ <% end %> +
+ <% end %> +
+
+ +
+ <%= if integration = @integrations[provider.id] do %> + + <% end %> + + +
+
+ + <%= if @configuring == provider.id do %> +
+ <.form + for={@form} + id={"#{provider.id}-form"} + phx-change="validate" + phx-submit="save" + > +
+ <.input + field={@form[:api_key]} + type="password" + label={t("API Key")} + placeholder={"Enter your #{provider.name} API key"} + autocomplete="off" + value={get_credential(@integrations[provider.id], "api_key")} + /> + + <.input + field={@form[:sync_interval_minutes]} + type="number" + label={t("Sync interval (minutes)")} + min="5" + value={ + if(@integrations[provider.id], + do: @integrations[provider.id].sync_interval_minutes, + else: 10 + ) + } + /> + + <%= if @test_result do %> +
"bg-green-50 dark:bg-green-900/20" + {:error, _} -> "bg-red-50 dark:bg-red-900/20" + end + ]}> +

"text-green-800 dark:text-green-200" + {:error, _} -> "text-red-800 dark:text-red-200" + end + ]}> + {elem(@test_result, 1)} +

+
+ <% end %> + +
+ +
+ + +
+
+
+ +
+ <% end %> +
+
+ <% end %>
diff --git a/test/towerops_web/live/org/integrations_live_test.exs b/test/towerops_web/live/org/integrations_live_test.exs index fd9100c6..3b0d180d 100644 --- a/test/towerops_web/live/org/integrations_live_test.exs +++ b/test/towerops_web/live/org/integrations_live_test.exs @@ -65,7 +65,7 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do assert integration.credentials["webhook_secret"] == "existing-secret-abc" end - test "displays webhook URL when gaiia integration exists", %{ + test "displays webhook URL when gaiia integration is configured", %{ conn: conn, user: user, organization: org @@ -77,15 +77,16 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do credentials: %{"api_key" => "key", "webhook_secret" => "secret123"} }) - {:ok, _view, html} = + {:ok, view, _html} = conn |> log_in_user(user) |> live(~p"/orgs/#{org.slug}/settings/integrations") + html = view |> element("#configure-gaiia") |> render_click() assert html =~ "/api/v1/webhooks/gaiia/#{org.id}" end - test "displays webhook secret when gaiia integration exists", %{ + test "displays webhook secret when gaiia integration is configured", %{ conn: conn, user: user, organization: org @@ -97,11 +98,12 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do credentials: %{"api_key" => "key", "webhook_secret" => "secret123"} }) - {:ok, _view, html} = + {:ok, view, _html} = conn |> log_in_user(user) |> live(~p"/orgs/#{org.slug}/settings/integrations") + html = view |> element("#configure-gaiia") |> render_click() assert html =~ "secret123" end @@ -122,6 +124,7 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do |> log_in_user(user) |> live(~p"/orgs/#{org.slug}/settings/integrations") + view |> element("#configure-gaiia") |> render_click() view |> element("#regenerate-webhook-secret") |> render_click() assert {:ok, integration} = Towerops.Integrations.get_integration(org.id, "gaiia") @@ -130,7 +133,7 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do assert integration.credentials["api_key"] == "key" end - test "shows setup instructions when gaiia integration exists", %{ + test "shows setup instructions when gaiia is being configured", %{ conn: conn, user: user, organization: org @@ -142,11 +145,12 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do credentials: %{"api_key" => "key", "webhook_secret" => "secret"} }) - {:ok, _view, html} = + {:ok, view, _html} = conn |> log_in_user(user) |> live(~p"/orgs/#{org.slug}/settings/integrations") + html = view |> element("#configure-gaiia") |> render_click() assert html =~ "Webhook Configuration" assert html =~ "Webhook URL" assert html =~ "Webhook Secret"