Each billing sync (Gaiia, Splynx, VISP, Sonar) already computes subscriber counts and MRR but only stored them in sync messages. Now persists them on the integrations table so the dashboard can aggregate across all billing providers instead of only querying Gaiia network sites (which were never populated). Also fixes SnmpKit Config environment detection to use Mix.env() as fallback when MIX_ENV env var is not set.
637 lines
30 KiB
Text
637 lines
30 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
>
|
|
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
|
|
<div class="mb-4">
|
|
<.link
|
|
navigate={~p"/orgs/#{@organization.slug}/settings"}
|
|
class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
|
>
|
|
<.icon name="hero-arrow-left" class="h-4 w-4" /> Back to Settings
|
|
</.link>
|
|
</div>
|
|
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
|
|
{t("Integrations")}
|
|
</h1>
|
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
{t("Connect third-party services to enhance your monitoring capabilities.")}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="mt-8 space-y-10">
|
|
<div :for={category <- @provider_categories}>
|
|
<div class="mb-4">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{category.name}
|
|
</h2>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
{category.description}
|
|
</p>
|
|
</div>
|
|
|
|
<%= 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) %>
|
|
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-white/10 dark:bg-white/5">
|
|
<%= if active_billing do %>
|
|
<%!-- Active billing provider --%>
|
|
<% integration = @integrations[active_billing.id] %>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex items-start gap-4">
|
|
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-50 dark:bg-indigo-900/30">
|
|
<.icon
|
|
name={active_billing.icon}
|
|
class="h-6 w-6 text-indigo-600 dark:text-indigo-400"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center gap-3">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{active_billing.name}
|
|
</h3>
|
|
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
|
Connected
|
|
</span>
|
|
</div>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
{active_billing.description}
|
|
</p>
|
|
|
|
<div class="mt-3 flex flex-wrap items-center gap-4">
|
|
<%= if integration.last_synced_at do %>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
{t("Last synced:")}
|
|
<.timestamp
|
|
datetime={integration.last_synced_at}
|
|
timezone={@timezone}
|
|
format="absolute"
|
|
/>
|
|
</span>
|
|
<% end %>
|
|
|
|
<%= if integration.last_sync_status && integration.last_sync_status != "never" do %>
|
|
<span class={[
|
|
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
|
|
case integration.last_sync_status do
|
|
"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)}
|
|
</span>
|
|
<% end %>
|
|
|
|
<%= if integration.last_synced_at do %>
|
|
<span class="text-xs text-gray-400 dark:text-gray-500">
|
|
· Next sync in ~{next_sync_minutes(
|
|
integration.last_synced_at,
|
|
integration.sync_interval_minutes
|
|
)}m
|
|
</span>
|
|
<% end %>
|
|
|
|
<%= if active_billing.id == "gaiia" do %>
|
|
<.link
|
|
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>
|
|
<.link
|
|
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 →
|
|
</.link>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
phx-click="toggle_enabled"
|
|
phx-value-provider={active_billing.id}
|
|
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-indigo-600 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
|
|
>
|
|
<span class="pointer-events-none inline-block h-5 w-5 translate-x-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
id={"configure-#{active_billing.id}"}
|
|
phx-click="configure"
|
|
phx-value-provider={active_billing.id}
|
|
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
|
|
>
|
|
{t("Configure")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Switch provider link --%>
|
|
<div class="mt-4 border-t border-gray-100 pt-3 dark:border-white/5">
|
|
<p class="text-xs text-gray-400 dark:text-gray-500">
|
|
Using {active_billing.name}. To switch providers, disable this integration first.
|
|
</p>
|
|
</div>
|
|
<% else %>
|
|
<%!-- No active billing — show provider picker --%>
|
|
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">
|
|
{t("Select your billing platform to get started:")}
|
|
</p>
|
|
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
<button
|
|
:for={provider <- category.providers}
|
|
type="button"
|
|
id={"configure-#{provider.id}"}
|
|
phx-click="configure"
|
|
phx-value-provider={provider.id}
|
|
class={[
|
|
"group relative flex flex-col items-center rounded-lg border-2 p-4 text-center transition-all hover:border-indigo-300 hover:bg-indigo-50/50 dark:hover:border-indigo-700 dark:hover:bg-indigo-900/10",
|
|
if(@configuring == provider.id,
|
|
do:
|
|
"border-indigo-500 bg-indigo-50/50 dark:border-indigo-600 dark:bg-indigo-900/20",
|
|
else: "border-gray-200 dark:border-white/10"
|
|
)
|
|
]}
|
|
>
|
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-50 group-hover:bg-indigo-100 dark:bg-indigo-900/30">
|
|
<.icon
|
|
name={provider.icon}
|
|
class="h-5 w-5 text-indigo-600 dark:text-indigo-400"
|
|
/>
|
|
</div>
|
|
<span class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
|
|
{provider.name}
|
|
</span>
|
|
<span class="mt-0.5 text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
|
|
{provider.description}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%!-- 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)) %>
|
|
<div class="mt-6 border-t border-gray-200 pt-6 dark:border-white/10">
|
|
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-4">
|
|
Configure {provider.name}
|
|
</h4>
|
|
<.form
|
|
for={@form}
|
|
id={"#{provider.id}-form"}
|
|
phx-change="validate"
|
|
phx-submit="save"
|
|
>
|
|
<input type="hidden" name="integration[provider]" value={provider.id} />
|
|
<div class="space-y-4">
|
|
<%= if provider.id in ["splynx", "sonar"] do %>
|
|
<.input
|
|
field={@form[:instance_url]}
|
|
type="text"
|
|
label={t("Instance URL")}
|
|
placeholder={"https://your-instance.#{provider.id}.com"}
|
|
autocomplete="off"
|
|
value={get_credential(@integrations[provider.id], "instance_url")}
|
|
/>
|
|
<% end %>
|
|
|
|
<%= if provider.id == "sonar" do %>
|
|
<.input
|
|
field={@form[:api_token]}
|
|
type="password"
|
|
label={t("API Token")}
|
|
placeholder="Enter your Sonar API token"
|
|
autocomplete="off"
|
|
value={get_credential(@integrations[provider.id], "api_token")}
|
|
/>
|
|
<% else %>
|
|
<.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")}
|
|
/>
|
|
<% end %>
|
|
|
|
<%= if provider.id == "splynx" do %>
|
|
<.input
|
|
field={@form[:api_secret]}
|
|
type="password"
|
|
label={t("API Secret")}
|
|
placeholder="Enter your Splynx API secret"
|
|
autocomplete="off"
|
|
value={get_credential(@integrations[provider.id], "api_secret")}
|
|
/>
|
|
<% end %>
|
|
|
|
<.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: if(provider.id == "gaiia", do: 15, else: 10)
|
|
)
|
|
}
|
|
/>
|
|
|
|
<%= if @test_result do %>
|
|
<div class={[
|
|
"rounded-md p-4",
|
|
case @test_result do
|
|
{:ok, _} -> "bg-green-50 dark:bg-green-900/20"
|
|
{:error, _} -> "bg-red-50 dark:bg-red-900/20"
|
|
end
|
|
]}>
|
|
<p class={[
|
|
"text-sm",
|
|
case @test_result do
|
|
{:ok, _} -> "text-green-800 dark:text-green-200"
|
|
{:error, _} -> "text-red-800 dark:text-red-200"
|
|
end
|
|
]}>
|
|
{elem(@test_result, 1)}
|
|
</p>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div class="flex items-center justify-between pt-4">
|
|
<button
|
|
type="button"
|
|
id="test-connection"
|
|
phx-click="test_connection"
|
|
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
|
|
>
|
|
{t("Test Connection")}
|
|
</button>
|
|
<div class="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
phx-click="close_config"
|
|
class="text-sm font-semibold text-gray-900 dark:text-white"
|
|
>
|
|
{t("Cancel")}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
phx-disable-with={t("Saving...")}
|
|
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
|
>
|
|
{t("Save")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</.form>
|
|
</div>
|
|
|
|
<%!-- Gaiia webhook config --%>
|
|
<%= if @configuring == "gaiia" && @integrations["gaiia"] do %>
|
|
<div class="mt-6 border-t border-gray-200 pt-6 dark:border-white/10">
|
|
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{t("Webhook Configuration")}
|
|
</h4>
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{t(
|
|
"Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change."
|
|
)}
|
|
</p>
|
|
|
|
<div class="mt-4 space-y-4">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300">
|
|
{t("Webhook URL")}
|
|
</label>
|
|
<div class="mt-1 flex items-center gap-2">
|
|
<input
|
|
id="gaiia-webhook-url"
|
|
type="text"
|
|
readonly
|
|
value={webhook_url(@organization.id)}
|
|
class="block w-full rounded-md border-gray-300 bg-gray-50 py-1.5 text-sm text-gray-900 shadow-xs sm:leading-6 dark:border-white/10 dark:bg-white/5 dark:text-white"
|
|
/>
|
|
<button
|
|
type="button"
|
|
phx-hook="CopyToClipboard"
|
|
data-target="#gaiia-webhook-url"
|
|
id="copy-webhook-url"
|
|
class="inline-flex items-center gap-1 rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/20"
|
|
>
|
|
<.icon name="hero-clipboard" class="h-3.5 w-3.5" /> Copy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300">
|
|
{t("Webhook Secret")}
|
|
</label>
|
|
<div class="mt-1 flex items-center gap-2">
|
|
<input
|
|
id="gaiia-webhook-secret"
|
|
type="text"
|
|
readonly
|
|
value={get_credential(@integrations["gaiia"], "webhook_secret")}
|
|
class="block w-full rounded-md border-gray-300 bg-gray-50 py-1.5 font-mono text-sm text-gray-900 shadow-xs sm:leading-6 dark:border-white/10 dark:bg-white/5 dark:text-white"
|
|
/>
|
|
<button
|
|
type="button"
|
|
phx-hook="CopyToClipboard"
|
|
data-target="#gaiia-webhook-secret"
|
|
id="copy-webhook-secret"
|
|
class="inline-flex items-center gap-1 rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/20"
|
|
>
|
|
<.icon name="hero-clipboard" class="h-3.5 w-3.5" /> Copy
|
|
</button>
|
|
</div>
|
|
<div class="mt-2">
|
|
<button
|
|
type="button"
|
|
id="regenerate-webhook-secret"
|
|
phx-click="regenerate_webhook_secret"
|
|
data-confirm={
|
|
t(
|
|
"Are you sure? Any existing Gaiia webhook using the current secret will stop working."
|
|
)
|
|
}
|
|
class="text-xs text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
|
>
|
|
{t("Regenerate Secret")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rounded-md bg-blue-50 p-3 dark:bg-blue-900/20">
|
|
<h5 class="text-xs font-medium text-blue-800 dark:text-blue-300">
|
|
{t("Setup Instructions")}
|
|
</h5>
|
|
<ol class="mt-2 list-inside list-decimal space-y-1 text-xs text-blue-700 dark:text-blue-400">
|
|
<li>In your Gaiia admin panel, go to Settings → Webhooks</li>
|
|
<li>Click "Add Webhook"</li>
|
|
<li>Paste the Webhook URL above</li>
|
|
<li>Paste the Webhook Secret above into the "Secret" field</li>
|
|
<li>Select events: Account, Billing Subscription, Inventory Item</li>
|
|
<li>Save the webhook</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|
|
</div>
|
|
<% else %>
|
|
<%!-- Non-exclusive categories: one card per provider --%>
|
|
<div class="space-y-4">
|
|
<div
|
|
:for={provider <- category.providers}
|
|
class="rounded-lg border border-gray-200 bg-white p-6 dark:border-white/10 dark:bg-white/5"
|
|
>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex items-start gap-4">
|
|
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-50 dark:bg-indigo-900/30">
|
|
<.icon
|
|
name={provider.icon}
|
|
class="h-6 w-6 text-indigo-600 dark:text-indigo-400"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{provider.name}
|
|
</h3>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
{provider.description}
|
|
</p>
|
|
|
|
<%= if integration = @integrations[provider.id] do %>
|
|
<div class="mt-3 flex flex-wrap items-center gap-4">
|
|
<span class={[
|
|
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
|
|
if(integration.enabled,
|
|
do:
|
|
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
|
|
else: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
|
)
|
|
]}>
|
|
{if integration.enabled, do: "Enabled", else: "Disabled"}
|
|
</span>
|
|
|
|
<%= if integration.last_synced_at do %>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
{t("Last synced:")}
|
|
<.timestamp
|
|
datetime={integration.last_synced_at}
|
|
timezone={@timezone}
|
|
format="absolute"
|
|
/>
|
|
</span>
|
|
<% end %>
|
|
|
|
<%= if integration.last_sync_status && integration.last_sync_status != "never" do %>
|
|
<span class={[
|
|
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
|
|
case integration.last_sync_status do
|
|
"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)}
|
|
</span>
|
|
<% end %>
|
|
|
|
<%= if integration.last_synced_at do %>
|
|
<span class="text-xs text-gray-400 dark:text-gray-500">
|
|
· Next sync in ~{next_sync_minutes(
|
|
integration.last_synced_at,
|
|
integration.sync_interval_minutes
|
|
)}m
|
|
</span>
|
|
<% 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>
|
|
<.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 →
|
|
</.link>
|
|
<% end %>
|
|
|
|
<%= if integration.enabled do %>
|
|
<div class="mt-3 w-full rounded-md bg-gray-50 p-3 dark:bg-gray-800/50">
|
|
<h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
|
<.icon name="hero-clock" class="h-3.5 w-3.5 inline" /> Sync Schedule
|
|
</h4>
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
<%= if provider.id == "preseem" do %>
|
|
Syncs every {integration.sync_interval_minutes} minutes. Baselines and fleet profiles computed nightly.
|
|
<% end %>
|
|
</p>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<%= if integration = @integrations[provider.id] do %>
|
|
<button
|
|
type="button"
|
|
phx-click="toggle_enabled"
|
|
phx-value-provider={provider.id}
|
|
class={[
|
|
"relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2",
|
|
if(integration.enabled,
|
|
do: "bg-indigo-600",
|
|
else: "bg-gray-200 dark:bg-gray-700"
|
|
)
|
|
]}
|
|
>
|
|
<span class={[
|
|
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out",
|
|
if(integration.enabled, do: "translate-x-5", else: "translate-x-0")
|
|
]} />
|
|
</button>
|
|
<% end %>
|
|
|
|
<button
|
|
type="button"
|
|
id={"configure-#{provider.id}"}
|
|
phx-click="configure"
|
|
phx-value-provider={provider.id}
|
|
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
|
|
>
|
|
{t("Configure")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<%= if @configuring == provider.id do %>
|
|
<div class="mt-6 border-t border-gray-200 pt-6 dark:border-white/10">
|
|
<.form
|
|
for={@form}
|
|
id={"#{provider.id}-form"}
|
|
phx-change="validate"
|
|
phx-submit="save"
|
|
>
|
|
<input type="hidden" name="integration[provider]" value={provider.id} />
|
|
<div class="space-y-4">
|
|
<.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 %>
|
|
<div class={[
|
|
"rounded-md p-4",
|
|
case @test_result do
|
|
{:ok, _} -> "bg-green-50 dark:bg-green-900/20"
|
|
{:error, _} -> "bg-red-50 dark:bg-red-900/20"
|
|
end
|
|
]}>
|
|
<p class={[
|
|
"text-sm",
|
|
case @test_result do
|
|
{:ok, _} -> "text-green-800 dark:text-green-200"
|
|
{:error, _} -> "text-red-800 dark:text-red-200"
|
|
end
|
|
]}>
|
|
{elem(@test_result, 1)}
|
|
</p>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div class="flex items-center justify-between pt-4">
|
|
<button
|
|
type="button"
|
|
id="test-connection"
|
|
phx-click="test_connection"
|
|
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
|
|
>
|
|
{t("Test Connection")}
|
|
</button>
|
|
<div class="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
phx-click="close_config"
|
|
class="text-sm font-semibold text-gray-900 dark:text-white"
|
|
>
|
|
{t("Cancel")}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
phx-disable-with={t("Saving...")}
|
|
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
|
>
|
|
{t("Save")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</.form>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</Layouts.authenticated>
|