mirror of
https://github.com/plausible/analytics.git
synced 2025-03-14 10:06:38 +00:00
Rework settings UI (#4626)
* Update generic components library * Import generic components via `PlausibleWeb` * Update Settings/Danger Zone * Update new shared link template and convert to heex * Update site settings layout * Update site settings sidebar tab layout * Update Settings/Email Reports * Update Funnels * Update ComboBox * Extend/update form components * Update Modal live component * Update Settings/Goals * Update Shields * Update Settings/Props * Update Settings/Import & Export * Update flow progress * Import Routes in settings * Update Billing components * Update Billing notice component * Update feature toggle component * Update 2fa component * Update verification markup * Update installation * Update Settings/Integrations/Plugins * Update domain change markup * Update Settings/General * Update Settings/Integrations * Update Settings/People * Update Settings/Integrations/GSC * Update Settings/Visiblity * ukuwip * ukuwip * Tables & paddings * Imports exports * Brighten disabled input text color for dark mode * Tune down table border/divider in dark mode * Format * Fix goal list on mobile * Fix IP Shields table on mobile * Fix country shields list on mobile * Fix country shield list on mobile * Fix page shields list on mobile * Fix import/export settings on mobile * Fix combobox dropdown background in dark mode * Fix filter bar search input on mobile * Revert @ukutaht's changes to goal list * Maybe maybe maybe * Revert the current prod goal list + fix mobile issues * Format * Revert tests change cc @ukutaht * Fix markup expectation in a test * Set autocomplete="off" again * Bring back `text-sm` where previously removed --------- Co-authored-by: Uku Taht <uku.taht@gmail.com>
This commit is contained in:
@ -59,13 +59,9 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"
|
||||
>
|
||||
<h2 class="text-xl font-black dark:text-gray-100 mb-6">
|
||||
<.title class="mb-6">
|
||||
<%= if @funnel, do: "Edit", else: "Add" %> Funnel
|
||||
</h2>
|
||||
|
||||
<label for={f[:name].name} class="block mb-3 font-medium dark:text-gray-100">
|
||||
Funnel Name
|
||||
</label>
|
||||
</.title>
|
||||
|
||||
<.input
|
||||
field={f[:name]}
|
||||
@ -73,13 +69,13 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
autocomplete="off"
|
||||
placeholder="e.g. From Blog to Purchase"
|
||||
autofocus
|
||||
class="w-full focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-900 dark:text-gray-300 block w-7/12 rounded-md sm:text-sm border-gray-300 dark:border-gray-500 w-full p-2 mt-2"
|
||||
label="Funnel Name"
|
||||
/>
|
||||
|
||||
<div id="steps-builder" class="mt-6">
|
||||
<label class="font-medium dark:text-gray-100">
|
||||
<.label>
|
||||
Funnel Steps
|
||||
</label>
|
||||
</.label>
|
||||
|
||||
<div :for={step_idx <- @step_ids} class="flex mb-3 mt-3">
|
||||
<div class="w-2/5 flex-1">
|
||||
@ -123,27 +119,21 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
<p id="funnel-eval" class="text-gray-500 dark:text-gray-400 text-sm mt-2 mb-2">
|
||||
<%= if @evaluation_result do %>
|
||||
Last month conversion rate: <strong><%= List.last(@evaluation_result.steps).conversion_rate %></strong>%
|
||||
<% else %>
|
||||
<span class="text-red-600 text-sm">
|
||||
Choose minimum <%= Funnel.min_steps() %> steps to evaluate funnel.
|
||||
</span>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<PlausibleWeb.Components.Generic.button
|
||||
id="save"
|
||||
type="submit"
|
||||
class="w-full"
|
||||
disabled={
|
||||
has_steps_errors?(f) or map_size(@selections_made) < Funnel.min_steps() or
|
||||
length(@step_ids) > map_size(@selections_made)
|
||||
}
|
||||
>
|
||||
<span><%= if @funnel, do: "Update", else: "Add" %> Funnel →</span>
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
<PlausibleWeb.Components.Generic.button
|
||||
id="save"
|
||||
type="submit"
|
||||
class="w-full"
|
||||
disabled={
|
||||
has_steps_errors?(f) or map_size(@selections_made) < Funnel.min_steps() or
|
||||
length(@step_ids) > map_size(@selections_made)
|
||||
}
|
||||
>
|
||||
<span><%= if @funnel, do: "Update", else: "Add" %> Funnel</span>
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
@ -200,7 +190,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
class="border-dotted border-b border-gray-400 "
|
||||
tooltip="Sample calculation for last month"
|
||||
>
|
||||
<span class="hidden md:inline">Entering Visitors: </span><strong><%= PlausibleWeb.StatsView.large_number_format(@result.entering_visitors) %></strong>
|
||||
<span class="hidden md:inline">Visitors: </span><strong><%= PlausibleWeb.StatsView.large_number_format(@result.entering_visitors) %></strong>
|
||||
</span>
|
||||
</span>
|
||||
<span :if={step && @at > 0}>
|
||||
|
@ -9,94 +9,47 @@ defmodule PlausibleWeb.Live.FunnelSettings.List do
|
||||
"""
|
||||
use Phoenix.LiveComponent
|
||||
use Phoenix.HTML
|
||||
import PlausibleWeb.Components.Generic
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<div class="border-t border-gray-200 pt-4 sm:flex sm:items-center sm:justify-between">
|
||||
<form id="filter-form" phx-change="filter">
|
||||
<div class="text-gray-800 text-sm inline-flex items-center">
|
||||
<div class="relative rounded-md shadow-sm flex">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<Heroicons.magnifying_glass class="feather mr-1 dark:text-gray-300" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="filter-text"
|
||||
id="filter-text"
|
||||
class="pl-8 shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800"
|
||||
placeholder="Search Funnels"
|
||||
value={@filter_text}
|
||||
/>
|
||||
</div>
|
||||
<.filter_bar filter_text={@filter_text} placeholder="Search Funnels">
|
||||
<.button id="add-funnel-button" phx-click="add-funnel" mt?={false}>
|
||||
Add Funnel
|
||||
</.button>
|
||||
</.filter_bar>
|
||||
|
||||
<Heroicons.backspace
|
||||
:if={String.trim(@filter_text) != ""}
|
||||
class="feather ml-2 cursor-pointer hover:text-red-500 dark:text-gray-300 dark:hover:text-red-500"
|
||||
phx-click="reset-filter-text"
|
||||
id="reset-filter"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4 flex sm:ml-4 sm:mt-0">
|
||||
<PlausibleWeb.Components.Generic.button phx-click="add-funnel">
|
||||
+ Add Funnel
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</div>
|
||||
<%= if Enum.count(@funnels) > 0 do %>
|
||||
<div class="mt-4">
|
||||
<%= for funnel <- @funnels do %>
|
||||
<div class="border-b border-gray-300 dark:border-gray-500 py-3 flex justify-between">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<%= funnel.name %>
|
||||
<span class="text-sm text-gray-400 font-normal block mt-1">
|
||||
<%= funnel.steps_count %>-step funnel
|
||||
</span>
|
||||
<.table rows={@funnels}>
|
||||
<:tbody :let={funnel}>
|
||||
<.td truncate>
|
||||
<span class="font-medium"><%= funnel.name %></span>
|
||||
</.td>
|
||||
<.td hide_on_mobile>
|
||||
<span class="text-gray-500 dark:text-gray-400">
|
||||
<%= funnel.steps_count %>-step funnel
|
||||
</span>
|
||||
<div class="flex items-center gap-x-4">
|
||||
<a href="#" phx-click="edit-funnel" phx-value-funnel-id={funnel.id}>
|
||||
<Heroicons.pencil_square class="feather feather-sm text-indigo-800 hover:text-indigo-500 dark:text-indigo-500 dark:hover:text-indigo-300" />
|
||||
</a>
|
||||
<button
|
||||
id={"delete-funnel-#{funnel.id}"}
|
||||
phx-click="delete-funnel"
|
||||
phx-value-funnel-id={funnel.id}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm={"Are you sure you want to remove funnel '#{funnel.name}'? This will just affect the UI, all of your analytics data will stay intact."}
|
||||
>
|
||||
<svg
|
||||
class="feather feather-sm"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
|
||||
</path>
|
||||
<line x1="10" y1="11" x2="10" y2="17"></line>
|
||||
<line x1="14" y1="11" x2="14" y2="17"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</.td>
|
||||
<.td actions>
|
||||
<.edit_button phx-click="edit-funnel" phx-value-funnel-id={funnel.id} />
|
||||
<.delete_button
|
||||
id={"delete-funnel-#{funnel.id}"}
|
||||
phx-click="delete-funnel"
|
||||
phx-value-funnel-id={funnel.id}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm={"Are you sure you want to remove funnel '#{funnel.name}'? This will just affect the UI, all of your analytics data will stay intact."}
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
<% else %>
|
||||
<p class="text-sm text-gray-800 dark:text-gray-200 mt-12 mb-8 text-center">
|
||||
<p class="mt-12 mb-8 text-sm text-center">
|
||||
<span :if={String.trim(@filter_text) != ""}>
|
||||
No funnels found for this site. Please refine or
|
||||
<a
|
||||
class="text-indigo-500 cursor-pointer underline"
|
||||
phx-click="reset-filter-text"
|
||||
id="reset-filter-hint"
|
||||
>
|
||||
<.styled_link phx-click="reset-filter-text" id="reset-filter-hint">
|
||||
reset your search.
|
||||
</a>
|
||||
</.styled_link>
|
||||
</span>
|
||||
<span :if={String.trim(@filter_text) == "" && Enum.empty?(@funnels)}>
|
||||
No funnels configured for this site.
|
||||
|
@ -13,6 +13,8 @@ defmodule PlausibleWeb do
|
||||
|
||||
alias PlausibleWeb.Router.Helpers, as: Routes
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
import PlausibleWeb.Components.Generic
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -79,14 +79,12 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
pad
|
||||
title="Pageviews"
|
||||
usage={@usage.pageviews}
|
||||
class="font-normal text-gray-500 dark:text-gray-400"
|
||||
/>
|
||||
<.usage_and_limits_row
|
||||
id={"custom_events_#{@period}"}
|
||||
pad
|
||||
title="Custom events"
|
||||
usage={@usage.custom_events}
|
||||
class="font-normal text-gray-500 dark:text-gray-400"
|
||||
/>
|
||||
</.usage_and_limits_table>
|
||||
"""
|
||||
@ -169,10 +167,10 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
def usage_and_limits_row(assigns) do
|
||||
~H"""
|
||||
<tr {@rest}>
|
||||
<td class={["py-4 pr-1 text-sm sm:whitespace-nowrap text-left", @pad && "pl-6"]}>
|
||||
<td class={["text-sm py-4 pr-1 sm:whitespace-nowrap text-left", @pad && "pl-6"]}>
|
||||
<%= @title %>
|
||||
</td>
|
||||
<td class="py-4 text-sm sm:whitespace-nowrap text-right">
|
||||
<td class="text-sm py-4 sm:whitespace-nowrap text-right">
|
||||
<%= Cldr.Number.to_string!(@usage) %>
|
||||
<%= if is_number(@limit), do: "/ #{Cldr.Number.to_string!(@limit)}" %>
|
||||
</td>
|
||||
@ -184,8 +182,7 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
~H"""
|
||||
<div
|
||||
id="monthly-quota-box"
|
||||
class="h-32 px-2 py-4 my-4 text-center bg-gray-100 rounded dark:bg-gray-900"
|
||||
style="width: 11.75rem;"
|
||||
class="w-1/3 h-32 px-2 py-4 my-4 text-center bg-gray-100 rounded dark:bg-gray-900 w-max-md"
|
||||
>
|
||||
<h4 class="font-black dark:text-gray-100">Monthly quota</h4>
|
||||
<div class="py-2 text-xl font-medium dark:text-gray-100">
|
||||
@ -245,7 +242,7 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
id={@id}
|
||||
onclick={"if (#{@confirmed}) {Paddle.Checkout.open(#{Jason.encode!(%{product: @paddle_product_id, email: @user.email, disableLogout: true, passthrough: @user.id, success: Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success), theme: "none"})})}"}
|
||||
class={[
|
||||
"w-full mt-6 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 text-white",
|
||||
"text-sm w-full mt-6 block rounded-md py-2 px-3 text-center font-semibold leading-6 text-white",
|
||||
!@checkout_disabled && "bg-indigo-600 hover:bg-indigo-500",
|
||||
@checkout_disabled && "pointer-events-none bg-gray-400 dark:bg-gray-600"
|
||||
]}
|
||||
|
@ -63,7 +63,6 @@ defmodule PlausibleWeb.Components.Billing.Notice do
|
||||
attr(:current_user, User, required: true)
|
||||
attr(:feature_mod, :atom, required: true, values: Feature.list())
|
||||
attr(:grandfathered?, :boolean, default: false)
|
||||
attr(:size, :atom, default: :sm)
|
||||
attr(:rest, :global)
|
||||
|
||||
def premium_feature(assigns) do
|
||||
@ -71,7 +70,6 @@ defmodule PlausibleWeb.Components.Billing.Notice do
|
||||
<.notice
|
||||
:if={@feature_mod.check_availability(@billable_user) !== :ok}
|
||||
class="rounded-t-md rounded-b-none"
|
||||
size={@size}
|
||||
title="Notice"
|
||||
{@rest}
|
||||
>
|
||||
|
@ -21,9 +21,9 @@ defmodule PlausibleWeb.Components.FlowProgress do
|
||||
|
||||
~H"""
|
||||
<div :if={not Enum.empty?(@steps)} class="mt-6 hidden md:block" id="flow-progress">
|
||||
<div class="flex items-center justify-between max-w-3xl mx-auto my-8">
|
||||
<div class="flex items-center justify-between max-w-4xl mx-auto my-8">
|
||||
<%= for {step, idx} <- Enum.with_index(@steps) do %>
|
||||
<div class="flex items-center text-xs">
|
||||
<div class="flex items-center text-base">
|
||||
<div
|
||||
:if={idx < @current_step_idx}
|
||||
class="w-5 h-5 bg-green-500 dark:bg-green-600 text-white rounded-full flex items-center justify-center"
|
||||
|
@ -5,17 +5,23 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
use Phoenix.Component, global_prefixes: ~w(x-)
|
||||
|
||||
@notice_themes %{
|
||||
gray: %{
|
||||
bg: "bg-white dark:bg-gray-800",
|
||||
icon: "text-gray-400",
|
||||
title_text: "text-gray-800 dark:text-gray-400",
|
||||
body_text: "text-gray-700 dark:text-gray-500 leading-5"
|
||||
},
|
||||
yellow: %{
|
||||
bg: "bg-yellow-50 dark:bg-yellow-100",
|
||||
icon: "text-yellow-400",
|
||||
title_text: "text-yellow-800 dark:text-yellow-900",
|
||||
body_text: "text-yellow-700 dark:text-yellow-800"
|
||||
title_text: "text-sm text-yellow-800 dark:text-yellow-900",
|
||||
body_text: "text-sm text-yellow-700 dark:text-yellow-800 leading-5"
|
||||
},
|
||||
red: %{
|
||||
bg: "bg-red-100",
|
||||
icon: "text-red-700",
|
||||
title_text: "text-red-800 dark:text-red-900",
|
||||
body_text: "text-red-700 dark:text-red-800"
|
||||
title_text: "text-sm text-red-800 dark:text-red-900",
|
||||
body_text: "text-sm text-red-700 dark:text-red-800"
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,15 +30,16 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
"bright" =>
|
||||
"border border-gray-200 bg-gray-100 dark:bg-gray-300 text-gray-800 hover:bg-gray-200 focus-visible:outline-gray-100",
|
||||
"danger" =>
|
||||
"border border-gray-300 dark:border-gray-500 text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:border-blue-300 active:text-red-800"
|
||||
"border border-gray-300 dark:border-gray-500 text-red-700 bg-white dark:bg-gray-900 hover:text-red-500 dark:hover:text-red-400 focus:border-blue-300 dark:text-red-500 active:text-red-800"
|
||||
}
|
||||
|
||||
@button_base_class "inline-flex items-center justify-center gap-x-2 rounded-md px-3.5 py-2.5 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:bg-gray-400 dark:disabled:text-white dark:disabled:text-gray-400 dark:disabled:bg-gray-700"
|
||||
@button_base_class "whitespace-nowrap truncate inline-flex items-center justify-center gap-x-2 font-medium rounded-md px-3.5 py-2.5 text-sm shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:bg-gray-400 dark:disabled:text-white dark:disabled:text-gray-400 dark:disabled:bg-gray-700"
|
||||
|
||||
attr(:type, :string, default: "button")
|
||||
attr(:theme, :string, default: "primary")
|
||||
attr(:class, :string, default: "")
|
||||
attr(:disabled, :boolean, default: false)
|
||||
attr(:mt?, :boolean, default: true)
|
||||
attr(:rest, :global)
|
||||
|
||||
slot(:inner_block)
|
||||
@ -49,6 +56,7 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
type={@type}
|
||||
disabled={@disabled}
|
||||
class={[
|
||||
@mt? && "mt-6",
|
||||
@button_base_class,
|
||||
@theme_class,
|
||||
@class
|
||||
@ -64,11 +72,26 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
attr(:class, :string, default: "")
|
||||
attr(:theme, :string, default: "primary")
|
||||
attr(:disabled, :boolean, default: false)
|
||||
attr(:method, :string, default: "get")
|
||||
attr(:mt?, :boolean, default: true)
|
||||
attr(:rest, :global)
|
||||
|
||||
slot(:inner_block)
|
||||
|
||||
def button_link(assigns) do
|
||||
extra =
|
||||
if assigns.method == "get" do
|
||||
[]
|
||||
else
|
||||
[
|
||||
"data-csrf": Phoenix.HTML.Tag.csrf_token_value(assigns.href),
|
||||
"data-method": assigns.method,
|
||||
"data-to": assigns.href
|
||||
]
|
||||
end
|
||||
|
||||
assigns = assign(assigns, extra: extra)
|
||||
|
||||
theme_class =
|
||||
if assigns.disabled do
|
||||
"bg-gray-400 text-white dark:text-white dark:text-gray-400 dark:bg-gray-700 cursor-not-allowed"
|
||||
@ -95,10 +118,12 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
href={@href}
|
||||
onclick={@onclick}
|
||||
class={[
|
||||
@mt? && "mt-6",
|
||||
@button_base_class,
|
||||
@theme_class,
|
||||
@class
|
||||
]}
|
||||
{@extra}
|
||||
{@rest}
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
@ -110,14 +135,13 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
|
||||
def docs_info(assigns) do
|
||||
~H"""
|
||||
<a href={"https://plausible.io/docs/#{@slug}"} rel="noreferrer" target="_blank">
|
||||
<Heroicons.information_circle class="text-gray-400 w-6 h-6 absolute top-0 right-0" />
|
||||
<a href={"https://plausible.io/docs/#{@slug}"} rel="noopener noreferrer" target="_blank">
|
||||
<Heroicons.information_circle class="text-gray-500 dark:text-indigo-500 w-6 h-6 stroke-2 absolute top-4 right-4 hover:text-indigo-500 dark:hover:text-indigo-300" />
|
||||
</a>
|
||||
"""
|
||||
end
|
||||
|
||||
attr(:title, :any, default: nil)
|
||||
attr(:size, :atom, default: :sm)
|
||||
attr(:theme, :atom, default: :yellow)
|
||||
attr(:dismissable_id, :any, default: nil)
|
||||
attr(:class, :string, default: "")
|
||||
@ -128,7 +152,7 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
assigns = assign(assigns, :theme, Map.fetch!(@notice_themes, assigns.theme))
|
||||
|
||||
~H"""
|
||||
<div id={@dismissable_id} class={@dismissable_id && "hidden"}>
|
||||
<div id={@dismissable_id} class={[@dismissable_id && "hidden"]}>
|
||||
<div class={["rounded-md p-4 relative", @theme.bg, @class]} {@rest}>
|
||||
<button
|
||||
:if={@dismissable_id}
|
||||
@ -153,10 +177,10 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
</svg>
|
||||
</div>
|
||||
<div class={["w-full", @title && "ml-3"]}>
|
||||
<h3 :if={@title} class={"text-#{@size} font-medium #{@theme.title_text} mb-2"}>
|
||||
<h3 :if={@title} class={"font-medium #{@theme.title_text} mb-2"}>
|
||||
<%= @title %>
|
||||
</h3>
|
||||
<div class={"text-#{@size} #{@theme.body_text}"}>
|
||||
<div class={"#{@theme.body_text}"}>
|
||||
<p>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</p>
|
||||
@ -176,13 +200,12 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
"""
|
||||
end
|
||||
|
||||
attr :id, :any, default: nil
|
||||
attr :href, :string, default: "#"
|
||||
attr :new_tab, :boolean, default: false
|
||||
attr :class, :string, default: ""
|
||||
attr :rest, :global
|
||||
attr :method, :string, default: "get"
|
||||
slot :inner_block
|
||||
attr(:href, :string, default: "#")
|
||||
attr(:new_tab, :boolean, default: false)
|
||||
attr(:class, :string, default: "")
|
||||
attr(:rest, :global)
|
||||
attr(:method, :string, default: "get")
|
||||
slot(:inner_block)
|
||||
|
||||
def styled_link(assigns) do
|
||||
~H"""
|
||||
@ -199,11 +222,11 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
end
|
||||
|
||||
slot :button, required: true do
|
||||
attr :class, :string
|
||||
attr(:class, :string)
|
||||
end
|
||||
|
||||
slot :panel, required: true do
|
||||
attr :class, :string
|
||||
attr(:class, :string)
|
||||
end
|
||||
|
||||
def dropdown(assigns) do
|
||||
@ -235,10 +258,10 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
"""
|
||||
end
|
||||
|
||||
attr :href, :string, required: true
|
||||
attr :new_tab, :boolean, default: false
|
||||
attr :rest, :global
|
||||
slot :inner_block, required: true
|
||||
attr(:href, :string, required: true)
|
||||
attr(:new_tab, :boolean, default: false)
|
||||
attr(:rest, :global)
|
||||
slot(:inner_block, required: true)
|
||||
|
||||
def dropdown_link(assigns) do
|
||||
class =
|
||||
@ -260,13 +283,12 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
"""
|
||||
end
|
||||
|
||||
attr :href, :string, required: true
|
||||
attr :new_tab, :boolean, default: false
|
||||
attr :class, :string, default: ""
|
||||
attr :id, :any, default: nil
|
||||
attr :rest, :global
|
||||
attr :method, :string, default: "get"
|
||||
slot :inner_block
|
||||
attr(:href, :string, required: true)
|
||||
attr(:new_tab, :boolean, default: false)
|
||||
attr(:class, :string, default: "")
|
||||
attr(:rest, :global)
|
||||
attr(:method, :string, default: "get")
|
||||
slot(:inner_block)
|
||||
|
||||
def unstyled_link(assigns) do
|
||||
extra =
|
||||
@ -287,7 +309,6 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
|
||||
~H"""
|
||||
<.link
|
||||
id={@id}
|
||||
class={[
|
||||
"inline-flex items-center gap-x-0.5",
|
||||
@class
|
||||
@ -311,8 +332,8 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
end
|
||||
end
|
||||
|
||||
attr :class, :any, default: ""
|
||||
attr :rest, :global
|
||||
attr(:class, :any, default: "")
|
||||
attr(:rest, :global)
|
||||
|
||||
def spinner(assigns) do
|
||||
~H"""
|
||||
@ -335,9 +356,54 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
"""
|
||||
end
|
||||
|
||||
attr :sticky?, :boolean, default: true
|
||||
def settings_tiles(assigns) do
|
||||
~H"""
|
||||
<div class="text-gray-900 leading-5 dark:text-gray-100">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :docs, :string, default: nil
|
||||
slot :inner_block, required: true
|
||||
slot :tooltip_content, required: true
|
||||
slot :title, required: true
|
||||
slot :subtitle, required: true
|
||||
attr :feature_mod, :atom, default: nil
|
||||
attr :site, :any
|
||||
attr :conn, :any
|
||||
|
||||
def tile(assigns) do
|
||||
~H"""
|
||||
<div class="shadow bg-white dark:bg-gray-800 rounded-md mb-6">
|
||||
<header class="relative py-4 px-6">
|
||||
<.title>
|
||||
<%= render_slot(@title) %>
|
||||
|
||||
<.docs_info :if={@docs} slug={@docs} />
|
||||
</.title>
|
||||
<div class="text-sm mt-px text-gray-500 dark:text-gray-400 leading-5">
|
||||
<%= render_slot(@subtitle) %>
|
||||
</div>
|
||||
<%= if @feature_mod do %>
|
||||
<PlausibleWeb.Components.Site.Feature.toggle
|
||||
feature_mod={@feature_mod}
|
||||
site={@site}
|
||||
conn={@conn}
|
||||
/>
|
||||
<% end %>
|
||||
<div class="border-b dark:border-gray-700 pb-4"></div>
|
||||
</header>
|
||||
|
||||
<div class="pb-4 px-6">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr(:sticky?, :boolean, default: true)
|
||||
slot(:inner_block, required: true)
|
||||
slot(:tooltip_content, required: true)
|
||||
|
||||
def tooltip(assigns) do
|
||||
wrapper_data =
|
||||
@ -348,7 +414,7 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
assigns = assign(assigns, wrapper_data: wrapper_data, show_inner: show_inner)
|
||||
|
||||
~H"""
|
||||
<div x-data={@wrapper_data} class="tooltip-wrapper w-full relative">
|
||||
<div x-data={@wrapper_data} class="tooltip-wrapper w-full relative z-[1000]">
|
||||
<div
|
||||
x-cloak
|
||||
x-show={@show_inner}
|
||||
@ -374,19 +440,19 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
"""
|
||||
end
|
||||
|
||||
attr :rest, :global, include: ~w(fill stroke stroke-width)
|
||||
attr :name, :atom, required: true
|
||||
attr :outline, :boolean, default: true
|
||||
attr :solid, :boolean, default: false
|
||||
attr :mini, :boolean, default: false
|
||||
attr(:rest, :global, include: ~w(fill stroke stroke-width))
|
||||
attr(:name, :atom, required: true)
|
||||
attr(:outline, :boolean, default: true)
|
||||
attr(:solid, :boolean, default: false)
|
||||
attr(:mini, :boolean, default: false)
|
||||
|
||||
def dynamic_icon(assigns) do
|
||||
apply(Heroicons, assigns.name, [assigns])
|
||||
end
|
||||
|
||||
attr :width, :integer, default: 100
|
||||
attr :height, :integer, default: 100
|
||||
attr :id, :string, default: "shuttle"
|
||||
attr(:width, :integer, default: 100)
|
||||
attr(:height, :integer, default: 100)
|
||||
attr(:id, :string, default: "shuttle")
|
||||
|
||||
defp icon_class(link_assigns) do
|
||||
if String.contains?(link_assigns[:class], "text-sm") or
|
||||
@ -397,11 +463,11 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
end
|
||||
end
|
||||
|
||||
slot :item, required: true
|
||||
slot(:item, required: true)
|
||||
|
||||
def focus_list(assigns) do
|
||||
~H"""
|
||||
<ol class="list-disc space-y-1 ml-4 mt-1 mb-4">
|
||||
<ol class="list-disc space-y-1 ml-4 text-sm">
|
||||
<li :for={item <- @item} class="marker:text-indigo-700 dark:marker:text-indigo-700">
|
||||
<%= render_slot(item) %>
|
||||
</li>
|
||||
@ -413,16 +479,21 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
slot :subtitle
|
||||
slot :inner_block, required: true
|
||||
slot :footer
|
||||
attr :rest, :global
|
||||
|
||||
def focus_box(assigns) do
|
||||
~H"""
|
||||
<div class="focus-box bg-white w-full max-w-lg mx-auto dark:bg-gray-800 text-black dark:text-gray-100 shadow-md rounded mb-4 mt-8">
|
||||
<div
|
||||
class="bg-white w-full max-w-lg mx-auto dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-md rounded-md mt-12"
|
||||
{@rest}
|
||||
>
|
||||
<div class="p-8">
|
||||
<h2 :if={@title != []} class="text-xl font-black dark:text-gray-100">
|
||||
<.title :if={@title != []}>
|
||||
<%= render_slot(@title) %>
|
||||
</h2>
|
||||
</.title>
|
||||
<div></div>
|
||||
|
||||
<div :if={@subtitle != []} class="mt-2 dark:text-gray-200">
|
||||
<div :if={@subtitle != []} class="text-sm mt-4 leading-6">
|
||||
<%= render_slot(@subtitle) %>
|
||||
</div>
|
||||
|
||||
@ -445,4 +516,223 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :rest, :global
|
||||
attr :width, :string, default: "min-w-full"
|
||||
attr :rows, :list, default: []
|
||||
slot :thead, required: false
|
||||
slot :tbody, required: true
|
||||
|
||||
def table(assigns) do
|
||||
~H"""
|
||||
<table :if={not Enum.empty?(@rows)} class={@width} {@rest}>
|
||||
<thead :if={@thead != []}>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
<%= render_slot(@thead) %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr :for={item <- @rows}>
|
||||
<%= render_slot(@tbody, item) %>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
"""
|
||||
end
|
||||
|
||||
slot :inner_block, required: true
|
||||
attr :truncate, :boolean, default: false
|
||||
attr :max_width, :string, default: ""
|
||||
attr :height, :string, default: ""
|
||||
attr :actions, :boolean, default: nil
|
||||
attr :hide_on_mobile, :boolean, default: nil
|
||||
attr :rest, :global
|
||||
|
||||
def td(assigns) do
|
||||
max_width =
|
||||
cond do
|
||||
assigns.max_width != "" -> assigns.max_width
|
||||
assigns.truncate -> "max-w-sm"
|
||||
true -> ""
|
||||
end
|
||||
|
||||
assigns = assign(assigns, max_width: max_width)
|
||||
|
||||
~H"""
|
||||
<td
|
||||
class={[
|
||||
@height,
|
||||
"text-sm px-6 py-3 first:pl-0 last:pr-0 whitespace-nowrap",
|
||||
@truncate && "truncate",
|
||||
@max_width,
|
||||
@actions && "flex text-right justify-end",
|
||||
@hide_on_mobile && "hidden md:table-cell"
|
||||
]}
|
||||
{@rest}
|
||||
>
|
||||
<div :if={@actions} class="flex gap-2">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
<div :if={!@actions}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</td>
|
||||
"""
|
||||
end
|
||||
|
||||
slot :inner_block, required: true
|
||||
attr :invisible, :boolean, default: false
|
||||
attr :hide_on_mobile, :boolean, default: nil
|
||||
|
||||
def th(assigns) do
|
||||
class =
|
||||
if assigns[:invisible] do
|
||||
"invisible"
|
||||
else
|
||||
"px-6 first:pl-0 last:pr-0 py-3 text-left text-sm font-medium"
|
||||
end
|
||||
|
||||
assigns = assign(assigns, class: class)
|
||||
|
||||
~H"""
|
||||
<th scope="col" class={[@hide_on_mobile && "hidden md:table-cell", @class]}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</th>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :set_to, :boolean, default: false
|
||||
attr :disabled?, :boolean, default: false
|
||||
slot :inner_block, required: true
|
||||
|
||||
def toggle_submit(assigns) do
|
||||
~H"""
|
||||
<div class="mt-4 mb-2 flex items-center">
|
||||
<button
|
||||
type="submit"
|
||||
class={[
|
||||
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full transition-colors ease-in-out duration-200 focus:outline-none focus:ring",
|
||||
if(@set_to, do: "bg-indigo-600", else: "bg-gray-200 dark:bg-gray-700"),
|
||||
if(@disabled?, do: "cursor-not-allowed")
|
||||
]}
|
||||
disabled={@disabled?}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class={[
|
||||
"inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-800 shadow transform transition ease-in-out duration-200",
|
||||
if(@set_to, do: "translate-x-5", else: "translate-x-0")
|
||||
]}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<span class={[
|
||||
"ml-2 font-medium leading-5 text-sm",
|
||||
if(@disabled?,
|
||||
do: "text-gray-500 dark:text-gray-400",
|
||||
else: "text-gray-900 dark:text-gray-100"
|
||||
)
|
||||
]}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</span>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :href, :string, default: nil
|
||||
attr :rest, :global, include: ~w(method disabled)
|
||||
|
||||
def edit_button(assigns) do
|
||||
if assigns[:href] do
|
||||
~H"""
|
||||
<.unstyled_link href={@href} {@rest}>
|
||||
<Heroicons.pencil_square class="w-5 h-5 text-indigo-800 hover:text-indigo-500 dark:text-indigo-500 dark:hover:text-indigo-300" />
|
||||
</.unstyled_link>
|
||||
"""
|
||||
else
|
||||
~H"""
|
||||
<button {@rest}>
|
||||
<Heroicons.pencil_square class="w-5 h-5 text-indigo-800 hover:text-indigo-500 dark:text-indigo-500 dark:hover:text-indigo-300" />
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
attr :href, :string, default: nil
|
||||
attr :rest, :global, include: ~w(method disabled)
|
||||
|
||||
def delete_button(assigns) do
|
||||
if assigns[:href] do
|
||||
~H"""
|
||||
<.unstyled_link href={@href} {@rest}>
|
||||
<Heroicons.trash class="w-5 h-5 text-red-800 hover:text-red-500 dark:text-red-500 dark:hover:text-red-400" />
|
||||
</.unstyled_link>
|
||||
"""
|
||||
else
|
||||
~H"""
|
||||
<button {@rest}>
|
||||
<Heroicons.trash class="w-5 h-5 text-red-800 hover:text-red-500 dark:text-red-500 dark:hover:text-red-400" />
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
attr :filter_text, :string, default: ""
|
||||
attr :placeholder, :string, default: ""
|
||||
attr :filtering_enabled?, :boolean, default: true
|
||||
slot :inner_block, required: false
|
||||
|
||||
def filter_bar(assigns) do
|
||||
~H"""
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="text-gray-800 inline-flex items-center">
|
||||
<div :if={@filtering_enabled?} class="relative rounded-md shadow-sm flex">
|
||||
<form id="filter-form" phx-change="filter" class="flex items-center">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<Heroicons.magnifying_glass class="feather mr-1 dark:text-gray-300" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="filter-text"
|
||||
id="filter-text"
|
||||
class="w-36 sm:w-full pl-8 text-sm shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800"
|
||||
placeholder={@placeholder}
|
||||
value={@filter_text}
|
||||
/>
|
||||
|
||||
<Heroicons.backspace
|
||||
:if={String.trim(@filter_text) != ""}
|
||||
class="feather ml-2 cursor-pointer hover:text-red-500 dark:text-gray-300 dark:hover:text-red-500"
|
||||
phx-click="reset-filter-text"
|
||||
id="reset-filter"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
slot :inner_block, required: true
|
||||
attr :class, :any, default: nil
|
||||
|
||||
def h2(assigns) do
|
||||
~H"""
|
||||
<h2 class={[@class || "font-semibold leading-6 text-gray-900 dark:text-gray-100"]}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</h2>
|
||||
"""
|
||||
end
|
||||
|
||||
slot :inner_block, required: true
|
||||
attr :class, :any, default: nil
|
||||
|
||||
def title(assigns) do
|
||||
~H"""
|
||||
<.h2 class={["text-lg font-medium text-gray-900 dark:text-gray-100 leading-7", @class]}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</.h2>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
@ -6,6 +6,7 @@ defmodule PlausibleWeb.Components.Settings do
|
||||
use Phoenix.HTML
|
||||
|
||||
import PlausibleWeb.Components.Generic
|
||||
alias PlausibleWeb.Router.Helpers, as: Routes
|
||||
|
||||
embed_templates("../templates/site/settings_search_console.html")
|
||||
end
|
||||
|
@ -8,6 +8,7 @@ defmodule PlausibleWeb.Components.Site.Feature do
|
||||
attr(:site, Plausible.Site, required: true)
|
||||
attr(:feature_mod, :atom, required: true, values: Plausible.Billing.Feature.list())
|
||||
attr(:conn, Plug.Conn, required: true)
|
||||
attr(:class, :any, default: nil)
|
||||
slot(:inner_block)
|
||||
|
||||
def toggle(assigns) do
|
||||
@ -18,25 +19,20 @@ defmodule PlausibleWeb.Components.Site.Feature do
|
||||
|
||||
~H"""
|
||||
<div>
|
||||
<div class="mt-4 mb-8 flex items-center">
|
||||
<.feature_button
|
||||
set_to={!@current_setting}
|
||||
<.form
|
||||
action={target(@site, @feature_mod.toggle_field(), @conn, !@current_setting)}
|
||||
method="put"
|
||||
for={nil}
|
||||
class={@class}
|
||||
>
|
||||
<PlausibleWeb.Components.Generic.toggle_submit
|
||||
set_to={@current_setting}
|
||||
disabled?={@disabled?}
|
||||
conn={@conn}
|
||||
site={@site}
|
||||
feature_mod={@feature_mod}
|
||||
/>
|
||||
|
||||
<span class={[
|
||||
"ml-2 text-sm font-medium leading-5 mb-1",
|
||||
if(assigns.disabled?,
|
||||
do: "text-gray-500 dark:text-gray-300",
|
||||
else: "text-gray-900 dark:text-gray-100"
|
||||
)
|
||||
]}>
|
||||
>
|
||||
Show <%= @feature_mod.display_name() %> in the Dashboard
|
||||
</span>
|
||||
</div>
|
||||
</PlausibleWeb.Components.Generic.toggle_submit>
|
||||
</.form>
|
||||
|
||||
<div :if={@current_setting}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
@ -48,28 +44,4 @@ defmodule PlausibleWeb.Components.Site.Feature do
|
||||
r = conn.request_path
|
||||
Routes.site_path(conn, :update_feature_visibility, site.domain, setting, r: r, set: set_to)
|
||||
end
|
||||
|
||||
defp feature_button(assigns) do
|
||||
~H"""
|
||||
<.form action={target(@site, @feature_mod.toggle_field(), @conn, @set_to)} method="put" for={nil}>
|
||||
<button
|
||||
type="submit"
|
||||
class={[
|
||||
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full transition-colors ease-in-out duration-200 focus:outline-none focus:ring",
|
||||
if(assigns.set_to, do: "bg-gray-200 dark:bg-gray-700", else: "bg-indigo-600"),
|
||||
if(assigns.disabled?, do: "cursor-not-allowed")
|
||||
]}
|
||||
disabled={@disabled?}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class={[
|
||||
"inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-800 shadow transform transition ease-in-out duration-200",
|
||||
if(assigns.set_to, do: "translate-x-0", else: "translate-x-5")
|
||||
]}
|
||||
/>
|
||||
</button>
|
||||
</.form>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
@ -27,7 +27,7 @@ defmodule PlausibleWeb.Components.TwoFactor do
|
||||
|
||||
def verify_2fa_input(assigns) do
|
||||
~H"""
|
||||
<div class={[@class, "flex justify-center sm:justify-start"]}>
|
||||
<div class={[@class, "flex items-center"]}>
|
||||
<%= Phoenix.HTML.Form.text_input(@form, @field,
|
||||
autocomplete: "off",
|
||||
class:
|
||||
@ -44,6 +44,7 @@ defmodule PlausibleWeb.Components.TwoFactor do
|
||||
<PlausibleWeb.Components.Generic.button
|
||||
type="submit"
|
||||
id={@id}
|
||||
mt?={false}
|
||||
class="rounded-l-none [&>span.label-enabled]:block [&>span.label-disabled]:hidden [&[disabled]>span.label-enabled]:hidden [&[disabled]>span.label-disabled]:block"
|
||||
>
|
||||
<span class="label-enabled pointer-events-none">
|
||||
|
@ -110,7 +110,7 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
|
||||
phx-target={@myself}
|
||||
phx-debounce={200}
|
||||
value={@display_value}
|
||||
class="[&.phx-change-loading+svg.spinner]:block border-none py-1 px-1 p-0 w-full inline-block rounded-md focus:outline-none focus:ring-0 text-sm"
|
||||
class="text-sm [&.phx-change-loading+svg.spinner]:block border-none py-1 px-1 p-0 w-full inline-block rounded-md focus:outline-none focus:ring-0"
|
||||
style="background-color: inherit;"
|
||||
required={@required}
|
||||
/>
|
||||
@ -183,7 +183,7 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
|
||||
id={"dropdown-#{@ref}"}
|
||||
x-show="isOpen"
|
||||
x-ref="suggestions"
|
||||
class="w-full dropdown z-50 absolute mt-1 max-h-60 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm dark:bg-gray-900"
|
||||
class="text-sm w-full dropdown z-50 absolute mt-1 max-h-60 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-900"
|
||||
>
|
||||
<.option
|
||||
:if={display_creatable_option?(assigns)}
|
||||
|
@ -17,10 +17,14 @@ defmodule PlausibleWeb.Live.Components.Form do
|
||||
<.input field={@form[:email]} type="email" />
|
||||
<.input name="my-input" errors={["oh no!"]} />
|
||||
"""
|
||||
|
||||
@default_input_class "text-sm dark:bg-gray-900 block pl-3.5 py-2.5 border-gray-300 dark:border-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md"
|
||||
|
||||
attr(:id, :any, default: nil)
|
||||
attr(:name, :any)
|
||||
attr(:label, :string, default: nil)
|
||||
attr(:value, :any)
|
||||
attr(:width, :string, default: "w-full")
|
||||
|
||||
attr(:type, :string,
|
||||
default: "text",
|
||||
@ -43,22 +47,44 @@ defmodule PlausibleWeb.Live.Components.Form do
|
||||
multiple pattern placeholder readonly required rows size step)
|
||||
)
|
||||
|
||||
attr(:class, :any, default: @default_input_class)
|
||||
|
||||
attr(:mt?, :boolean, default: true)
|
||||
slot(:inner_block)
|
||||
|
||||
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
|
||||
assigns
|
||||
|> assign(field: nil, id: assigns.id || field.id)
|
||||
|> assign(
|
||||
field: nil,
|
||||
id: assigns.id || field.id,
|
||||
class: assigns.class,
|
||||
mt?: assigns.mt?,
|
||||
width: assigns.width
|
||||
)
|
||||
|> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
|
||||
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|
||||
|> assign_new(:value, fn -> field.value end)
|
||||
|> input()
|
||||
end
|
||||
|
||||
def input(%{type: "select"} = assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@name} class={@mt? && "mt-2"}>
|
||||
<.label for={@id} class="mb-2"><%= @label %></.label>
|
||||
<select id={@id} name={@name} multiple={@multiple} class={[@class, @width]} {@rest}>
|
||||
<option :if={@prompt} value=""><%= @prompt %></option>
|
||||
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
|
||||
</select>
|
||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# All other inputs text, datetime-local, url, password, etc. are handled here...
|
||||
def input(assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@name}>
|
||||
<.label :if={@label != nil and @label != ""} for={@id}>
|
||||
<div phx-feedback-for={@name} class={@mt? && "mt-2"}>
|
||||
<.label :if={@label != nil and @label != ""} for={@id} class="mb-2">
|
||||
<%= @label %>
|
||||
</.label>
|
||||
<input
|
||||
@ -66,6 +92,7 @@ defmodule PlausibleWeb.Live.Components.Form do
|
||||
name={@name}
|
||||
id={@id}
|
||||
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
|
||||
class={[@class, @width, assigns[:rest][:disabled] && "text-gray-500 dark:text-gray-400"]}
|
||||
{@rest}
|
||||
/>
|
||||
<%= render_slot(@inner_block) %>
|
||||
@ -78,33 +105,36 @@ defmodule PlausibleWeb.Live.Components.Form do
|
||||
|
||||
attr(:rest, :global)
|
||||
attr(:id, :string, required: true)
|
||||
attr(:class, :string, default: "")
|
||||
attr(:name, :string, required: true)
|
||||
attr(:label, :string, required: true)
|
||||
attr(:label, :string, default: nil)
|
||||
attr(:value, :string, default: "")
|
||||
|
||||
def input_with_clipboard(assigns) do
|
||||
class = [@default_input_class, "pr-20 w-full"]
|
||||
assigns = assign(assigns, class: class)
|
||||
|
||||
~H"""
|
||||
<div class="my-4">
|
||||
<div>
|
||||
<.label for={@id}>
|
||||
<div>
|
||||
<div :if={@label}>
|
||||
<.label for={@id} class="mb-2">
|
||||
<%= @label %>
|
||||
</.label>
|
||||
</div>
|
||||
<div class="relative mt-1">
|
||||
<div class="relative">
|
||||
<.input
|
||||
mt?={false}
|
||||
id={@id}
|
||||
name={@name}
|
||||
value={@value}
|
||||
type="text"
|
||||
readonly="readonly"
|
||||
class={[@class, "pr-20"]}
|
||||
class={@class}
|
||||
{@rest}
|
||||
/>
|
||||
<a
|
||||
onclick={"var input = document.getElementById('#{@id}'); input.focus(); input.select(); document.execCommand('copy'); event.stopPropagation();"}
|
||||
href="javascript:void(0)"
|
||||
class="absolute flex items-center text-xs font-medium text-indigo-600 no-underline hover:underline top-2 right-4"
|
||||
class="absolute flex items-center text-xs font-medium text-indigo-600 no-underline hover:underline top-3 right-4"
|
||||
>
|
||||
<Heroicons.document_duplicate class="pr-1 text-indigo-600 dark:text-indigo-500 w-5 h-5" />
|
||||
<span>
|
||||
@ -257,12 +287,13 @@ defmodule PlausibleWeb.Live.Components.Form do
|
||||
@doc """
|
||||
Renders a label.
|
||||
"""
|
||||
attr(:for, :string, default: nil)
|
||||
slot(:inner_block, required: true)
|
||||
attr :for, :string, default: nil
|
||||
slot :inner_block, required: true
|
||||
attr :class, :string, default: ""
|
||||
|
||||
def label(assigns) do
|
||||
~H"""
|
||||
<label for={@for} class="block font-medium dark:text-gray-100">
|
||||
<label for={@for} class={["text-sm block font-medium dark:text-gray-100", @class]}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</label>
|
||||
"""
|
||||
|
@ -171,7 +171,7 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
~H"""
|
||||
<div
|
||||
id={@id}
|
||||
class="relative z-[49] [&[data-phx-ref]_div.modal-dialog]:hidden [&[data-phx-ref]_div.modal-loading]:block"
|
||||
class="relative z-[2049] [&[data-phx-ref]_div.modal-dialog]:hidden [&[data-phx-ref]_div.modal-loading]:block"
|
||||
data-modal
|
||||
x-cloak
|
||||
x-data="{
|
||||
@ -223,12 +223,12 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="bg-opacity-75"
|
||||
x-transition:leave-end="bg-opacity-0"
|
||||
class="fixed inset-0 bg-gray-500 bg-opacity-75 z-50"
|
||||
class="fixed inset-0 bg-gray-500 bg-opacity-75 z-[2050]"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
x-show="modalPreopen"
|
||||
class="fixed flex inset-0 items-start z-50 overflow-y-auto overflow-x-hidden"
|
||||
class="fixed flex inset-0 items-start z-[2050] overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
<div class="modal-pre-loading w-full self-center">
|
||||
<div class="text-center">
|
||||
@ -238,7 +238,7 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
</div>
|
||||
<div
|
||||
x-show="modalOpen"
|
||||
class="fixed flex inset-0 items-start z-50 overflow-y-auto overflow-x-hidden"
|
||||
class="fixed flex inset-0 items-start z-[2050] overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
<Phoenix.Component.focus_wrap
|
||||
:if={@load_content?}
|
||||
|
@ -53,30 +53,30 @@ defmodule PlausibleWeb.Live.Components.Verification do
|
||||
<Heroicons.exclamation_triangle class="h-6 w-6 text-red-600 bg-red-100 dark:bg-red-200 dark:text-red-800" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<h3 class="font-semibold leading-6 text-xl">
|
||||
<div class="mt-8">
|
||||
<.title>
|
||||
<span :if={@finished? and @success?}>Success!</span>
|
||||
<span :if={not @finished?}>Verifying your installation</span>
|
||||
|
||||
<span :if={@finished? and not @success? and @interpretation}>
|
||||
<%= List.first(@interpretation.errors) %>
|
||||
</span>
|
||||
</h3>
|
||||
<p :if={@finished? and @success?} id="progress" class="mt-2">
|
||||
</.title>
|
||||
<p :if={@finished? and @success?} id="progress" class="text-sm mt-4">
|
||||
Your installation is working and visitors are being counted accurately
|
||||
</p>
|
||||
<p
|
||||
:if={@finished? and @success? and @awaiting_first_pageview?}
|
||||
id="progress"
|
||||
class="mt-2 animate-pulse"
|
||||
class="text-sm mt-4 animate-pulse"
|
||||
>
|
||||
Awaiting your first pageview
|
||||
</p>
|
||||
<p :if={not @finished?} class="mt-2 animate-pulse" id="progress"><%= @message %></p>
|
||||
<p :if={not @finished?} class="text-sm mt-4 animate-pulse" id="progress"><%= @message %></p>
|
||||
|
||||
<p
|
||||
:if={@finished? and not @success? and @interpretation}
|
||||
class="mt-2 text-ellipsis overflow-hidden"
|
||||
class="mt-4 text-sm text-ellipsis overflow-hidden"
|
||||
id="recommendation"
|
||||
>
|
||||
<span><%= List.first(@interpretation.recommendations).text %>. </span>
|
||||
@ -86,12 +86,13 @@ defmodule PlausibleWeb.Live.Components.Verification do
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div :if={@finished?} class="mt-8">
|
||||
<.button_link :if={not @success?} href="#" phx-click="retry" class="w-full">
|
||||
<div :if={@finished?} class="mt-6">
|
||||
<.button_link :if={not @success?} mt?={false} href="#" phx-click="retry" class="w-full">
|
||||
Verify installation again
|
||||
</.button_link>
|
||||
<.button_link
|
||||
:if={@success?}
|
||||
mt?={false}
|
||||
href={"/#{URI.encode_www_form(@domain)}?skip_to_dashboard=true"}
|
||||
class="w-full font-bold mb-4"
|
||||
>
|
||||
@ -127,11 +128,11 @@ defmodule PlausibleWeb.Live.Components.Verification do
|
||||
</.focus_list>
|
||||
<div
|
||||
:if={@verification_state && @super_admin? && @finished?}
|
||||
class="flex flex-col dark:text-gray-200 border-t border-gray-300 dark:border-gray-700"
|
||||
class="flex flex-col dark:text-gray-200 mt-4 pt-4 border-t border-gray-300 dark:border-gray-700"
|
||||
x-data="{ showDiagnostics: false }"
|
||||
id="super-admin-report"
|
||||
>
|
||||
<p class="mt-4 text-sm">
|
||||
<p class="text-sm">
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="showDiagnostics = !showDiagnostics"
|
||||
|
@ -5,7 +5,6 @@ defmodule PlausibleWeb.Live.CSVExport do
|
||||
use PlausibleWeb, :live_view
|
||||
use Phoenix.HTML
|
||||
|
||||
alias PlausibleWeb.Components.Generic
|
||||
alias Plausible.Exports
|
||||
|
||||
# :not_mounted_at_router ensures we have already done auth checks in the controller
|
||||
@ -117,10 +116,10 @@ defmodule PlausibleWeb.Live.CSVExport do
|
||||
|
||||
defp prepare_download(assigns) do
|
||||
~H"""
|
||||
<Generic.button phx-click="export">Prepare download</Generic.button>
|
||||
<p class="text-sm mt-4 text-gray-500">
|
||||
Prepare your data for download by clicking the button above. When that's done, a Zip file that you can download will appear.
|
||||
<p class="text-sm">
|
||||
Prepare your data for download by clicking the button below. When that's done, a Zip file that you can download will appear.
|
||||
</p>
|
||||
<.button phx-click="export">Prepare download</.button>
|
||||
"""
|
||||
end
|
||||
|
||||
@ -128,8 +127,8 @@ defmodule PlausibleWeb.Live.CSVExport do
|
||||
~H"""
|
||||
<div class="flex items-center justify-between space-x-2">
|
||||
<div class="flex items-center">
|
||||
<Generic.spinner />
|
||||
<span class="ml-2">We are preparing your download ...</span>
|
||||
<.spinner />
|
||||
<span class="ml-2 text-sm">We are preparing your download ...</span>
|
||||
</div>
|
||||
<button
|
||||
phx-click="cancel"
|
||||
@ -139,7 +138,7 @@ defmodule PlausibleWeb.Live.CSVExport do
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm mt-4 text-gray-500">
|
||||
<p class="text-sm">
|
||||
The preparation of your stats might take a while. Depending on the volume of your data, it might take up to 20 minutes. Feel free to leave the page and return later.
|
||||
</p>
|
||||
"""
|
||||
@ -147,12 +146,9 @@ defmodule PlausibleWeb.Live.CSVExport do
|
||||
|
||||
defp fetch_export_failed(assigns) do
|
||||
~H"""
|
||||
<div class="flex items-center">
|
||||
<Heroicons.exclamation_circle class="w-4 h-4 text-red-500" />
|
||||
<p class="ml-2 text-sm text-gray-500">
|
||||
Something went wrong when fetching exports. Please try again later.
|
||||
</p>
|
||||
</div>
|
||||
<.notice title="Something went wrong when fetching exports" theme={:red}>
|
||||
Please try again later.
|
||||
</.notice>
|
||||
"""
|
||||
end
|
||||
|
||||
@ -160,7 +156,7 @@ defmodule PlausibleWeb.Live.CSVExport do
|
||||
~H"""
|
||||
<div class="flex items-center">
|
||||
<Heroicons.exclamation_circle class="w-4 h-4 text-red-500" />
|
||||
<p class="ml-2 text-sm text-gray-500">
|
||||
<p class="ml-2 text-sm">
|
||||
Something went wrong when preparing your download. Please
|
||||
<button phx-click="export" class="text-indigo-500">try again.</button>
|
||||
</p>
|
||||
@ -170,28 +166,34 @@ defmodule PlausibleWeb.Live.CSVExport do
|
||||
|
||||
defp download(assigns) do
|
||||
~H"""
|
||||
<div class="flex items-center justify-between space-x-2">
|
||||
<a href={@href} class="inline-flex items-center">
|
||||
<Heroicons.document_text class="w-4 h-4" />
|
||||
<span class="ml-1 text-indigo-500"><%= @export.name %></span>
|
||||
</a>
|
||||
<button
|
||||
phx-click="delete"
|
||||
class="text-red-500 font-semibold"
|
||||
data-confirm="Are you sure you want to delete this export?"
|
||||
>
|
||||
<Heroicons.trash class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<.table rows={[@export]}>
|
||||
<:thead>
|
||||
<.th>Export</.th>
|
||||
<.th invisible>Actions</.th>
|
||||
</:thead>
|
||||
<:tbody :let={export}>
|
||||
<.td>
|
||||
<.styled_link href={@href}>
|
||||
<%= export.name %>
|
||||
</.styled_link>
|
||||
</.td>
|
||||
<.td actions>
|
||||
<.delete_button
|
||||
phx-click="delete"
|
||||
data-confirm="Are you sure you want to delete this export?"
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
|
||||
<p :if={@export.expires_at} class="text-sm mt-4 text-gray-500">
|
||||
Note that this file will expire
|
||||
<p :if={@export.expires_at} class="text-sm">
|
||||
Note: this file will expire
|
||||
<.hint message={@export.expires_at}>
|
||||
<%= Timex.Format.DateTime.Formatters.Relative.format!(@export.expires_at, "{relative}") %>.
|
||||
</.hint>
|
||||
</p>
|
||||
|
||||
<p :if={@storage == "local"} class="text-sm mt-4 text-gray-500">
|
||||
<p :if={@storage == "local"} class="text-sm">
|
||||
Located at
|
||||
<.hint message={@export.path}><%= format_path(@export.path) %></.hint>
|
||||
(<%= format_bytes(@export.size) %>)
|
||||
|
@ -101,7 +101,7 @@ defmodule PlausibleWeb.Live.CSVImport do
|
||||
phx-drop-target={@upload.ref}
|
||||
class="block border-2 dark:border-gray-600 rounded-md p-4 hover:bg-gray-50 dark:hover:bg-gray-900 hover:border-indigo-500 dark:hover:border-indigo-600 transition cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center text-gray-500 dark:text-gray-500">
|
||||
<div class="hidden md:flex items-center text-gray-500 dark:text-gray-500">
|
||||
<Heroicons.document_plus class="w-5 h-5 transition" />
|
||||
<span class="ml-1.5 text-sm">
|
||||
(or drag-and-drop your unzipped CSVs here)
|
||||
@ -109,7 +109,7 @@ defmodule PlausibleWeb.Live.CSVImport do
|
||||
<.live_file_input upload={@upload} class="hidden" />
|
||||
</div>
|
||||
|
||||
<ul id="imported-tables" class="mt-3.5 mb-0.5 space-y-1.5">
|
||||
<ul id="imported-tables" class="truncate mt-3.5 mb-0.5 space-y-1.5">
|
||||
<.imported_table
|
||||
:for={{table, upload} <- @imported_tables}
|
||||
table={table}
|
||||
@ -123,20 +123,13 @@ defmodule PlausibleWeb.Live.CSVImport do
|
||||
|
||||
defp confirm_button(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
type="submit"
|
||||
disabled={not @can_confirm?}
|
||||
class={[
|
||||
"rounded-md w-full bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:bg-gray-400 dark:disabled:text-gray-400 dark:disabled:bg-gray-700 mt-4",
|
||||
unless(@can_confirm?, do: "cursor-not-allowed")
|
||||
]}
|
||||
>
|
||||
<.button type="submit" disabled={not @can_confirm?} class="w-full">
|
||||
<%= if @date_range do %>
|
||||
Confirm import <.dates range={@date_range} />
|
||||
<% else %>
|
||||
Confirm import
|
||||
<% end %>
|
||||
</button>
|
||||
</.button>
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -6,6 +6,7 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
use Plausible
|
||||
|
||||
import PlausibleWeb.Live.Components.Form
|
||||
import PlausibleWeb.Components.Generic
|
||||
|
||||
alias PlausibleWeb.Live.Components.ComboBox
|
||||
alias Plausible.Repo
|
||||
@ -68,9 +69,7 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
def edit_form(assigns) do
|
||||
~H"""
|
||||
<.form :let={f} for={@form} phx-submit="save-goal" phx-target={@myself}>
|
||||
<h2 class="text-xl font-black dark:text-gray-100">
|
||||
Edit Goal for <%= @domain %>
|
||||
</h2>
|
||||
<.title>Edit Goal for <%= @domain %></.title>
|
||||
|
||||
<.custom_event_fields
|
||||
:if={@selected_tab == "custom_events"}
|
||||
@ -91,11 +90,9 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
site={@site}
|
||||
/>
|
||||
|
||||
<div class="py-4">
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
Update Goal →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
<.button type="submit" class="w-full">
|
||||
Update Goal
|
||||
</.button>
|
||||
</.form>
|
||||
"""
|
||||
end
|
||||
@ -109,14 +106,9 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
phx-submit="save-goal"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<PlausibleWeb.Components.Generic.spinner
|
||||
class="spinner block absolute right-9 top-8"
|
||||
x-show="tabSelectionInProgress"
|
||||
/>
|
||||
<.spinner class="spinner block absolute right-9 top-8" x-show="tabSelectionInProgress" />
|
||||
|
||||
<h2 class="text-xl font-black dark:text-gray-100">
|
||||
Add Goal for <%= @domain %>
|
||||
</h2>
|
||||
<.title>Add Goal for <%= @domain %></.title>
|
||||
|
||||
<.tabs selected_tab={@selected_tab} myself={@myself} />
|
||||
|
||||
@ -141,16 +133,16 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
x-init="tabSelectionInProgress = false"
|
||||
/>
|
||||
|
||||
<div class="py-4" x-show="!tabSelectionInProgress">
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
Add Goal →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
<div x-show="!tabSelectionInProgress">
|
||||
<.button type="submit" class="w-full">
|
||||
Add Goal
|
||||
</.button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
:if={@selected_tab == "custom_events" && @event_name_options_count > 0}
|
||||
x-show="!tabSelectionInProgress"
|
||||
class="mt-2 text-sm hover:underline text-indigo-600 dark:text-indigo-400 text-left"
|
||||
class="mt-4 text-sm hover:underline text-indigo-600 dark:text-indigo-400 text-left"
|
||||
phx-click="autoconfigure"
|
||||
phx-target={@myself}
|
||||
>
|
||||
@ -195,20 +187,14 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
<%= msg %>
|
||||
</.error>
|
||||
|
||||
<div class="mt-2">
|
||||
<.label for="pageview_display_name_input">
|
||||
Display Name
|
||||
</.label>
|
||||
|
||||
<.input
|
||||
id="pageview_display_name_input"
|
||||
field={@f[:display_name]}
|
||||
type="text"
|
||||
x-data="{ firstFocus: true }"
|
||||
x-on:focus="if (firstFocus) { $el.select(); firstFocus = false; }"
|
||||
class="mt-2 dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<.input
|
||||
label="Display Name"
|
||||
id="pageview_display_name_input"
|
||||
field={@f[:display_name]}
|
||||
type="text"
|
||||
x-data="{ firstFocus: true }"
|
||||
x-on:focus="if (firstFocus) { $el.select(); firstFocus = false; }"
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
@ -228,13 +214,11 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
~H"""
|
||||
<div id="custom-events-form" class="my-6" {@rest}>
|
||||
<div id="event-fields">
|
||||
<div class="pb-6 text-xs text-gray-700 dark:text-gray-200 text-justify rounded-md">
|
||||
Custom Events are not tracked by default - you have to configure them on your site to be sent to Plausible. See examples and learn more in <a
|
||||
class="text-indigo-500 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://plausible.io/docs/custom-event-goals"
|
||||
> our docs</a>.
|
||||
<div class="text-sm pb-6 text-gray-500 dark:text-gray-400 text-justify rounded-md">
|
||||
Custom Events are not tracked by default - you have to configure them on your site to be sent to Plausible. See examples and learn more in
|
||||
<.styled_link href="https://plausible.io/docs/custom-event-goals" new_tab={true}>
|
||||
our docs
|
||||
</.styled_link>.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -263,17 +247,13 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<.label for="custom_event_display_name_input">
|
||||
Display Name
|
||||
</.label>
|
||||
|
||||
<.input
|
||||
label="Display Name"
|
||||
id="custom_event_display_name_input"
|
||||
field={@f[:display_name]}
|
||||
type="text"
|
||||
x-data="{ firstFocus: true }"
|
||||
x-on:focus="if (firstFocus) { $el.select(); firstFocus = false; }"
|
||||
class="mt-2 dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -306,7 +286,6 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
billable_user={@site.owner}
|
||||
current_user={@current_user}
|
||||
feature_mod={Plausible.Billing.Feature.RevenueGoals}
|
||||
size={:xs}
|
||||
class="rounded-b-md"
|
||||
/>
|
||||
<button
|
||||
@ -340,10 +319,10 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
</span>
|
||||
<span
|
||||
class={[
|
||||
"ml-3 font-medium",
|
||||
"ml-3 text-sm font-medium",
|
||||
if(@has_access_to_revenue_goals?,
|
||||
do: "text-gray-900 dark:text-gray-100",
|
||||
else: "text-gray-500 dark:text-gray-300"
|
||||
else: "text-gray-500 dark:text-gray-400"
|
||||
)
|
||||
]}
|
||||
id="enable-revenue-tracking"
|
||||
@ -377,8 +356,8 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
|
||||
def tabs(assigns) do
|
||||
~H"""
|
||||
<div class="mt-6 font-medium dark:text-gray-100">Goal Trigger</div>
|
||||
<div class="my-3 w-full flex rounded border border-gray-300 dark:border-gray-500">
|
||||
<div class="text-sm mt-6 font-medium dark:text-gray-100">Goal Trigger</div>
|
||||
<div class="my-2 text-sm w-full flex rounded border border-gray-300 dark:border-gray-500">
|
||||
<.custom_events_tab selected?={@selected_tab == "custom_events"} myself={@myself} />
|
||||
<.pageviews_tab selected?={@selected_tab == "pageviews"} myself={@myself} />
|
||||
</div>
|
||||
@ -389,9 +368,9 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
~H"""
|
||||
<a
|
||||
class={[
|
||||
"w-1/2 text-center py-2 border-r dark:border-gray-500",
|
||||
"w-1/2 text-center py-2.5 border-r dark:border-gray-500",
|
||||
"cursor-pointer",
|
||||
@selected? && "shadow-inner font-bold bg-indigo-600 text-white",
|
||||
@selected? && "shadow-inner font-medium bg-indigo-600 text-white",
|
||||
!@selected? && "dark:text-gray-100 text-gray-800"
|
||||
]}
|
||||
id="event-tab"
|
||||
@ -409,8 +388,8 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
~H"""
|
||||
<a
|
||||
class={[
|
||||
"w-1/2 text-center py-2 cursor-pointer",
|
||||
@selected? && "shadow-inner font-bold bg-indigo-600 text-white",
|
||||
"w-1/2 text-center py-2.5 cursor-pointer",
|
||||
@selected? && "shadow-inner font-medium bg-indigo-600 text-white",
|
||||
!@selected? && "dark:text-gray-100 text-gray-800"
|
||||
]}
|
||||
id="pageview-tab"
|
||||
|
@ -6,6 +6,7 @@ defmodule PlausibleWeb.Live.GoalSettings.List do
|
||||
use Phoenix.HTML
|
||||
|
||||
alias PlausibleWeb.Live.Components.Modal
|
||||
import PlausibleWeb.Components.Generic
|
||||
|
||||
attr(:goals, :list, required: true)
|
||||
attr(:domain, :string, required: true)
|
||||
@ -18,123 +19,79 @@ defmodule PlausibleWeb.Live.GoalSettings.List do
|
||||
|
||||
~H"""
|
||||
<div>
|
||||
<div class="border-t border-gray-200 pt-4 sm:flex sm:items-center sm:justify-between">
|
||||
<form id="filter-form" phx-change="filter">
|
||||
<div class="text-gray-800 text-sm inline-flex items-center">
|
||||
<div class="relative rounded-md shadow-sm flex">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<Heroicons.magnifying_glass class="feather mr-1 dark:text-gray-300" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="filter-text"
|
||||
id="filter-text"
|
||||
class="pl-8 shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800"
|
||||
placeholder="Search Goals"
|
||||
value={@filter_text}
|
||||
/>
|
||||
</div>
|
||||
<.filter_bar filter_text={@filter_text} placeholder="Search Goals">
|
||||
<.button
|
||||
id="add-goal-button"
|
||||
phx-click="add-goal"
|
||||
mt?={false}
|
||||
x-data
|
||||
x-on:click={Modal.JS.preopen("goals-form-modal")}
|
||||
>
|
||||
Add Goal
|
||||
</.button>
|
||||
</.filter_bar>
|
||||
|
||||
<Heroicons.backspace
|
||||
:if={String.trim(@filter_text) != ""}
|
||||
class="feather ml-2 cursor-pointer hover:text-red-500 dark:text-gray-300 dark:hover:text-red-500"
|
||||
phx-click="reset-filter-text"
|
||||
id="reset-filter"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4 flex sm:ml-4 sm:mt-0">
|
||||
<PlausibleWeb.Components.Generic.button
|
||||
id="add-goal-button"
|
||||
phx-click="add-goal"
|
||||
x-data
|
||||
x-on:click={Modal.JS.preopen("goals-form-modal")}
|
||||
>
|
||||
+ Add Goal
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</div>
|
||||
<%= if Enum.count(@goals) > 0 do %>
|
||||
<div class="mt-12">
|
||||
<%= for goal <- @goals do %>
|
||||
<div class="border-b border-gray-300 dark:border-gray-500 py-3 flex justify-between items-center h-16">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 w-2/3 cursor-help pr-4">
|
||||
<div class="flex" title={goal.page_path || goal.event_name}>
|
||||
<div class="truncate block">
|
||||
<div class="text-xs text-gray-400 block mb-1 font-normal">
|
||||
<.goal_description goal={goal} revenue_goals_enabled?={@revenue_goals_enabled?} />
|
||||
<.table rows={@goals}>
|
||||
<:tbody :let={goal}>
|
||||
<.td truncate max_width="max-w-40" height="h-16">
|
||||
<div class="flex">
|
||||
<div class="truncate block">
|
||||
<%= if not @revenue_goals_enabled? && goal.currency do %>
|
||||
<div class="truncate">
|
||||
<%= goal %>
|
||||
<br />
|
||||
<span class="text-red-600">
|
||||
Unlock Revenue Goals by upgrading to a business plan
|
||||
</span>
|
||||
</div>
|
||||
<%= if not @revenue_goals_enabled? && goal.currency do %>
|
||||
<div class="text-gray-600 flex items-center">
|
||||
<Heroicons.lock_closed class="w-4 h-4 mr-1 inline" />
|
||||
<div class="truncate"><%= goal %></div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="truncate"><%= goal %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<.goal_description goal={goal} />
|
||||
<span><%= goal %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<div class="flex items-center w-1/3">
|
||||
<div class="text-xs w-full mr-6 text-gray-400">
|
||||
<div class="hidden md:block">
|
||||
<div :if={goal.page_path} class="text-gray-600">Pageview</div>
|
||||
<div :if={goal.event_name && !goal.currency} class="text-gray-600">
|
||||
Custom Event
|
||||
</div>
|
||||
<div :if={goal.currency} class="text-gray-600">
|
||||
Revenue Goal (<%= goal.currency %>)
|
||||
</div>
|
||||
<div :if={not Enum.empty?(goal.funnels)}>Belongs to funnel(s)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
:if={!goal.currency || (goal.currency && @revenue_goals_enabled?)}
|
||||
x-data
|
||||
x-on:click={Modal.JS.preopen("goals-form-modal")}
|
||||
phx-click="edit-goal"
|
||||
phx-value-goal-id={goal.id}
|
||||
id={"edit-goal-#{goal.id}"}
|
||||
class="mr-4"
|
||||
>
|
||||
<Heroicons.pencil_square class="feather feather-sm text-indigo-800 hover:text-indigo-500 dark:text-indigo-500 dark:hover:text-indigo-300" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
:if={goal.currency && !@revenue_goals_enabled?}
|
||||
id={"edit-goal-#{goal.id}-disabled"}
|
||||
disabled
|
||||
class="mr-4 cursor-not-allowed"
|
||||
>
|
||||
<Heroicons.pencil_square class="feather feather-sm text-gray-400 dark:text-gray-600" />
|
||||
</button>
|
||||
<button
|
||||
id={"delete-goal-#{goal.id}"}
|
||||
phx-click="delete-goal"
|
||||
phx-value-goal-id={goal.id}
|
||||
phx-value-goal-name={goal.event_name}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm={delete_confirmation_text(goal)}
|
||||
>
|
||||
<Heroicons.trash class="feather feather-sm" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</.td>
|
||||
<.td hide_on_mobile height="h-16">
|
||||
<span :if={goal.page_path}>Pageview</span><span :if={goal.event_name && !goal.currency}>Custom Event</span><span :if={
|
||||
goal.currency
|
||||
}>Revenue Goal (<%= goal.currency %>)</span><span
|
||||
:if={not Enum.empty?(goal.funnels)}
|
||||
class="text-gray-400 dark:text-gray-600"
|
||||
><br />Belongs to funnel(s)</span>
|
||||
</.td>
|
||||
<.td actions height="h-16">
|
||||
<.edit_button
|
||||
:if={!goal.currency || (goal.currency && @revenue_goals_enabled?)}
|
||||
x-data
|
||||
x-on:click={Modal.JS.preopen("goals-form-modal")}
|
||||
phx-click="edit-goal"
|
||||
phx-value-goal-id={goal.id}
|
||||
id={"edit-goal-#{goal.id}"}
|
||||
/>
|
||||
<.edit_button
|
||||
:if={goal.currency && !@revenue_goals_enabled?}
|
||||
id={"edit-goal-#{goal.id}-disabled"}
|
||||
disabled
|
||||
class="cursor-not-allowed"
|
||||
/>
|
||||
<.delete_button
|
||||
id={"delete-goal-#{goal.id}"}
|
||||
phx-click="delete-goal"
|
||||
phx-value-goal-id={goal.id}
|
||||
phx-value-goal-name={goal.event_name}
|
||||
data-confirm={delete_confirmation_text(goal)}
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
<% else %>
|
||||
<p class="text-sm text-gray-800 dark:text-gray-200 mt-12 mb-8 text-center">
|
||||
<p class="mt-12 mb-8 text-center text-sm">
|
||||
<span :if={String.trim(@filter_text) != ""}>
|
||||
No goals found for this site. Please refine or
|
||||
<a
|
||||
class="text-indigo-500 cursor-pointer underline"
|
||||
phx-click="reset-filter-text"
|
||||
id="reset-filter-hint"
|
||||
>
|
||||
<.styled_link phx-click="reset-filter-text" id="reset-filter-hint">
|
||||
reset your search.
|
||||
</a>
|
||||
</.styled_link>
|
||||
</span>
|
||||
<span :if={String.trim(@filter_text) == "" && Enum.empty?(@goals)}>
|
||||
No goals configured for this site.
|
||||
@ -155,22 +112,18 @@ defmodule PlausibleWeb.Live.GoalSettings.List do
|
||||
end
|
||||
|
||||
def custom_event_description(goal) do
|
||||
if goal.display_name == goal.event_name, do: "", else: goal.event_name
|
||||
if goal.display_name == goal.event_name, do: "", else: "#{goal.event_name}"
|
||||
end
|
||||
|
||||
def goal_description(assigns) do
|
||||
~H"""
|
||||
<span :if={@goal.page_path} class="block w-full truncate">
|
||||
<span :if={@goal.page_path} class="block truncate text-gray-400 dark:text-gray-600">
|
||||
<%= pageview_description(@goal) %>
|
||||
</span>
|
||||
|
||||
<span :if={@goal.event_name}>
|
||||
<span :if={@goal.event_name} class="block truncate text-gray-400 dark:text-gray-600">
|
||||
<%= custom_event_description(@goal) %>
|
||||
</span>
|
||||
|
||||
<span :if={@goal.currency && not @revenue_goals_enabled?} class="text-red-600">
|
||||
Unlock Revenue Goals by upgrading to a business plan
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -66,128 +66,101 @@ defmodule PlausibleWeb.Live.ImportsExportsSettings do
|
||||
)
|
||||
|
||||
~H"""
|
||||
<div class="mt-5 flex gap-x-4">
|
||||
<.button_link
|
||||
class="w-36 h-20"
|
||||
theme="bright"
|
||||
disabled={@import_in_progress? or @at_maximum?}
|
||||
href={Plausible.Google.API.import_authorize_url(@site.id)}
|
||||
>
|
||||
<img src="/images/icon/google_analytics_logo.svg" alt="Google Analytics import" />
|
||||
</.button_link>
|
||||
<.notice :if={@import_warning} theme={:gray}>
|
||||
<%= @import_warning %>
|
||||
</.notice>
|
||||
|
||||
<div class="mt-4 flex justify-end gap-x-4">
|
||||
<.button_link
|
||||
class="w-36 h-20"
|
||||
theme="bright"
|
||||
href={Plausible.Google.API.import_authorize_url(@site.id)}
|
||||
disabled={@import_in_progress? or @at_maximum?}
|
||||
>
|
||||
Import from
|
||||
<img
|
||||
src="/images/icon/google_analytics_logo.svg"
|
||||
alt="Google Analytics import"
|
||||
class="h-6 w-12"
|
||||
/>
|
||||
</.button_link>
|
||||
<.button_link
|
||||
disabled={@import_in_progress? or @at_maximum?}
|
||||
href={"/#{URI.encode_www_form(@site.domain)}/settings/import"}
|
||||
>
|
||||
<img class="h-16" src="/images/icon/csv_logo.svg" alt="New CSV import" />
|
||||
Import from CSV
|
||||
</.button_link>
|
||||
</div>
|
||||
|
||||
<p :if={@import_warning} class="mt-4 text-gray-400 text-sm italic">
|
||||
<%= @import_warning %>
|
||||
<p :if={Enum.empty?(@site_imports)} class="text-center text-sm mt-8 mb-12">
|
||||
There are no imports yet for this site.
|
||||
</p>
|
||||
|
||||
<header class="relative border-b border-gray-200 pb-4">
|
||||
<h3 class="mt-6 text-md leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
Existing Imports
|
||||
</h3>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
A maximum of <%= @max_imports %> imports at any time is allowed.
|
||||
</p>
|
||||
</header>
|
||||
<div class="mt-8">
|
||||
<.table :if={not Enum.empty?(@site_imports)} rows={@site_imports}>
|
||||
<:thead>
|
||||
<.th>Import</.th>
|
||||
<.th hide_on_mobile>Date Range</.th>
|
||||
<.th hide_on_mobile>
|
||||
<div class="text-right">Pageviews</div>
|
||||
</.th>
|
||||
<.th invisible>Actions</.th>
|
||||
</:thead>
|
||||
|
||||
<div
|
||||
:if={Enum.empty?(@site_imports)}
|
||||
class="text-gray-800 dark:text-gray-200 text-center mt-8 mb-12"
|
||||
>
|
||||
<p>There are no imports yet for this site.</p>
|
||||
<:tbody :let={entry}>
|
||||
<.td max_width="max-w-40">
|
||||
<div class="flex items-center gap-x-2 truncate">
|
||||
<div class="w-6" title={notice_message(entry.tooltip)}>
|
||||
<Heroicons.clock
|
||||
:if={entry.live_status == SiteImport.pending()}
|
||||
class="block h-6 w-6 text-indigo-600 dark:text-green-600"
|
||||
/>
|
||||
<.spinner
|
||||
:if={entry.live_status == SiteImport.importing()}
|
||||
class="block h-6 w-6 text-indigo-600 dark:text-green-600"
|
||||
/>
|
||||
<Heroicons.check
|
||||
:if={entry.live_status == SiteImport.completed()}
|
||||
class="block h-6 w-6 text-indigo-600 dark:text-green-600"
|
||||
/>
|
||||
<Heroicons.exclamation_triangle
|
||||
:if={entry.live_status == SiteImport.failed()}
|
||||
class="block h-6 w-6 text-red-700 dark:text-red-500"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="max-w-sm"
|
||||
title={"#{Plausible.Imported.SiteImport.label(entry.site_import)} created at #{format_date(entry.site_import.inserted_at)}"}
|
||||
>
|
||||
<%= Plausible.Imported.SiteImport.label(entry.site_import) %>
|
||||
</div>
|
||||
</div>
|
||||
</.td>
|
||||
|
||||
<.td hide_on_mobile>
|
||||
<%= format_date(entry.site_import.start_date) %> - <%= format_date(
|
||||
entry.site_import.end_date
|
||||
) %>
|
||||
</.td>
|
||||
|
||||
<.td>
|
||||
<div class="text-right">
|
||||
<%= if entry.live_status == SiteImport.completed(),
|
||||
do:
|
||||
PlausibleWeb.StatsView.large_number_format(
|
||||
pageview_count(entry.site_import, @pageview_counts)
|
||||
) %>
|
||||
</div>
|
||||
</.td>
|
||||
<.td actions>
|
||||
<.delete_button
|
||||
href={"/#{URI.encode_www_form(@site.domain)}/settings/forget-import/#{entry.site_import.id}"}
|
||||
method="delete"
|
||||
data-confirm="Are you sure you want to delete this import?"
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
</div>
|
||||
<ul :if={not Enum.empty?(@site_imports)}>
|
||||
<li :for={entry <- @site_imports} class="py-4 flex items-center justify-between space-x-4">
|
||||
<.import_entry entry={entry} site={@site} pageview_counts={@pageview_counts} />
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
end
|
||||
|
||||
defp import_entry(assigns) do
|
||||
label_class =
|
||||
if assigns.entry.live_status != SiteImport.failed() do
|
||||
"ml-2"
|
||||
end
|
||||
|
||||
assigns = assign(assigns, :label_class, label_class)
|
||||
|
||||
~H"""
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center text-sm leading-5 font-medium text-gray-900 dark:text-gray-100">
|
||||
<Heroicons.clock
|
||||
:if={@entry.live_status == SiteImport.pending()}
|
||||
class="block h-6 w-5 text-indigo-600 dark:text-green-600"
|
||||
/>
|
||||
<.spinner
|
||||
:if={@entry.live_status == SiteImport.importing()}
|
||||
class="block h-6 w-5 text-indigo-600 dark:text-green-600"
|
||||
/>
|
||||
<Heroicons.check
|
||||
:if={@entry.live_status == SiteImport.completed()}
|
||||
class="block h-6 w-5 text-indigo-600 dark:text-green-600"
|
||||
/>
|
||||
<Heroicons.exclamation_triangle
|
||||
:if={@entry.live_status == SiteImport.failed()}
|
||||
class="block h-6 w-5 text-red-700 dark:text-red-700"
|
||||
/>
|
||||
<div :if={@entry.live_status == SiteImport.failed()} class="ml-2 mr-1">
|
||||
Import failed -
|
||||
</div>
|
||||
<.tooltip :if={@entry.tooltip}>
|
||||
<%= Plausible.Imported.SiteImport.label(@entry.site_import) %>
|
||||
<:tooltip_content>
|
||||
<.notice_message message_label={@entry.tooltip} />
|
||||
</:tooltip_content>
|
||||
</.tooltip>
|
||||
<div :if={!@entry.tooltip} class={[@label_class]}>
|
||||
<%= Plausible.Imported.SiteImport.label(@entry.site_import) %>
|
||||
</div>
|
||||
<div :if={@entry.live_status == SiteImport.completed()} class="text-xs font-normal ml-1">
|
||||
(<%= PlausibleWeb.StatsView.large_number_format(
|
||||
pageview_count(@entry.site_import, @pageview_counts)
|
||||
) %> page views)
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
From <%= format_date(@entry.site_import.start_date) %> to <%= format_date(
|
||||
@entry.site_import.end_date
|
||||
) %>
|
||||
<%= if @entry.live_status == SiteImport.completed() do %>
|
||||
(imported
|
||||
<% else %>
|
||||
(started
|
||||
<% end %>
|
||||
on <%= format_date(@entry.site_import.inserted_at) %>)
|
||||
</div>
|
||||
</div>
|
||||
<.button
|
||||
data-to={"/#{URI.encode_www_form(@site.domain)}/settings/forget-import/#{@entry.site_import.id}"}
|
||||
theme="danger"
|
||||
data-method="delete"
|
||||
data-csrf={Plug.CSRFProtection.get_csrf_token()}
|
||||
class="sm:ml-3 sm:w-auto w-full"
|
||||
data-confirm="Are you sure you want to delete this import?"
|
||||
>
|
||||
<span :if={@entry.live_status == SiteImport.completed()}>
|
||||
Delete Import
|
||||
</span>
|
||||
<span :if={@entry.live_status == SiteImport.failed()}>
|
||||
Discard
|
||||
</span>
|
||||
<span :if={@entry.live_status not in [SiteImport.completed(), SiteImport.failed()]}>
|
||||
Cancel Import
|
||||
</span>
|
||||
</.button>
|
||||
"""
|
||||
end
|
||||
|
||||
@ -243,8 +216,8 @@ defmodule PlausibleWeb.Live.ImportsExportsSettings do
|
||||
end
|
||||
end
|
||||
|
||||
defp notice_message(%{message_label: :slow_import} = assigns) do
|
||||
~H"""
|
||||
defp notice_message(:slow_import) do
|
||||
"""
|
||||
The import process might be taking longer due to the amount of data
|
||||
and rate limiting enforced by Google Analytics.
|
||||
"""
|
||||
|
@ -105,10 +105,10 @@ defmodule PlausibleWeb.Live.Installation do
|
||||
<PlausibleWeb.Components.FirstDashboardLaunchBanner.set :if={@site_created?} site={@site} />
|
||||
<PlausibleWeb.Components.FlowProgress.render flow={@flow} current_step="Install Plausible" />
|
||||
|
||||
<PlausibleWeb.Components.Generic.focus_box>
|
||||
<.focus_box>
|
||||
<:title :if={is_nil(@installation_type)}>
|
||||
<div class="flex w-full mx-auto justify-center">
|
||||
<PlausibleWeb.Components.Generic.spinner class="spinner block text-center h-8 w-8" />
|
||||
<.spinner class="spinner block text-center h-8 w-8" />
|
||||
</div>
|
||||
</:title>
|
||||
<:title :if={@installation_type == "WordPress"}>
|
||||
@ -233,7 +233,7 @@ defmodule PlausibleWeb.Live.Installation do
|
||||
</.styled_link>
|
||||
if you prefer manual installation method.
|
||||
</:footer>
|
||||
</PlausibleWeb.Components.Generic.focus_box>
|
||||
</.focus_box>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
@ -283,7 +283,7 @@ defmodule PlausibleWeb.Live.Installation do
|
||||
|
||||
defp script_extension_control(assigns) do
|
||||
~H"""
|
||||
<div class="mt-2 p-1">
|
||||
<div class="mt-2 p-1 text-sm">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -322,7 +322,7 @@ defmodule PlausibleWeb.Live.Installation do
|
||||
<div class="relative">
|
||||
<textarea
|
||||
id="snippet"
|
||||
class="w-full border-1 border-gray-300 rounded-md p-4 text-gray-700 0 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300"
|
||||
class="w-full border-1 border-gray-300 rounded-md p-4 text-sm text-gray-700 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300"
|
||||
rows="5"
|
||||
readonly
|
||||
><%= render_snippet(@installation_type, @domain, @script_config) %></textarea>
|
||||
@ -339,7 +339,7 @@ defmodule PlausibleWeb.Live.Installation do
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h3 class="text-normal mt-4 font-semibold">Enable optional measurements:</h3>
|
||||
<.h2 class="mt-8 text-sm font-medium">Enable optional measurements:</.h2>
|
||||
<.script_extension_control
|
||||
config={@script_config}
|
||||
variant="outbound-links"
|
||||
|
@ -8,6 +8,7 @@ defmodule PlausibleWeb.Live.Plugins.API.Settings do
|
||||
|
||||
alias Plausible.Sites
|
||||
alias Plausible.Plugins.API.Tokens
|
||||
import PlausibleWeb.Components.Generic
|
||||
|
||||
def mount(_params, %{"domain" => domain} = session, socket) do
|
||||
socket =
|
||||
@ -29,89 +30,58 @@ defmodule PlausibleWeb.Live.Plugins.API.Settings do
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.flash_messages flash={@flash} />
|
||||
<div>
|
||||
<.flash_messages flash={@flash} />
|
||||
|
||||
<%= if @add_token? do %>
|
||||
<%= live_render(
|
||||
@socket,
|
||||
PlausibleWeb.Live.Plugins.API.TokenForm,
|
||||
id: "token-form",
|
||||
session: %{
|
||||
"domain" => @domain,
|
||||
"token_description" => @token_description,
|
||||
"rendered_by" => self()
|
||||
}
|
||||
) %>
|
||||
<% end %>
|
||||
<%= if @add_token? do %>
|
||||
<%= live_render(
|
||||
@socket,
|
||||
PlausibleWeb.Live.Plugins.API.TokenForm,
|
||||
id: "token-form",
|
||||
session: %{
|
||||
"domain" => @domain,
|
||||
"token_description" => @token_description,
|
||||
"rendered_by" => self()
|
||||
}
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="border-t border-gray-200 pt-4 grid">
|
||||
<div class="mt-4 sm:ml-4 sm:mt-0 justify-self-end">
|
||||
<PlausibleWeb.Components.Generic.button phx-click="add-token">
|
||||
+ Add Plugin Token
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<.filter_bar filtering_enabled?={false}>
|
||||
<.button phx-click="add-token" mt?={false}>
|
||||
Add Plugin Token
|
||||
</.button>
|
||||
</.filter_bar>
|
||||
|
||||
<div
|
||||
:if={not Enum.empty?(@displayed_tokens)}
|
||||
class="mt-8 overflow-hidden border-b border-gray-200 shadow dark:border-gray-900 sm:rounded-lg"
|
||||
>
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-900">
|
||||
<thead class="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
Description
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
Hint
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
Last used
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
<span class="sr-only">Revoke</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for token <- @displayed_tokens do %>
|
||||
<tr class="bg-white dark:bg-gray-800">
|
||||
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<span class="token-description">
|
||||
<%= token.description %>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-100 font-mono">
|
||||
**********<%= token.hint %>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm font-normal whitespace-nowrap">
|
||||
<%= Plausible.Plugins.API.Token.last_used_humanize(token) %>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm font-medium text-right">
|
||||
<button
|
||||
id={"revoke-token-#{token.id}"}
|
||||
phx-click="revoke-token"
|
||||
phx-value-token-id={token.id}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm="Are you sure you want to revoke this Token? This action cannot be reversed."
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<.table :if={not Enum.empty?(@displayed_tokens)} rows={@displayed_tokens}>
|
||||
<:thead>
|
||||
<.th>Description</.th>
|
||||
<.th hide_on_mobile>Hint</.th>
|
||||
<.th hide_on_mobile>Last used</.th>
|
||||
<.th invisible>Actions</.th>
|
||||
</:thead>
|
||||
<:tbody :let={token}>
|
||||
<.td>
|
||||
<span class="token-description">
|
||||
<%= token.description %>
|
||||
</span>
|
||||
</.td>
|
||||
<.td hide_on_mobile>
|
||||
**********<%= token.hint %>
|
||||
</.td>
|
||||
<.td hide_on_mobile>
|
||||
<%= Plausible.Plugins.API.Token.last_used_humanize(token) %>
|
||||
</.td>
|
||||
<.td actions>
|
||||
<.delete_button
|
||||
id={"revoke-token-#{token.id}"}
|
||||
phx-click="revoke-token"
|
||||
phx-value-token-id={token.id}
|
||||
data-confirm="Are you sure you want to revoke this Token? This action cannot be reversed."
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
@ -54,41 +54,41 @@ defmodule PlausibleWeb.Live.Plugins.API.TokenForm do
|
||||
phx-submit="save-token"
|
||||
phx-click-away="cancel-add-token"
|
||||
>
|
||||
<h2 class="text-xl font-black dark:text-gray-100 mb-8">
|
||||
<.title>
|
||||
Add Plugin Token for <%= @domain %>
|
||||
</h2>
|
||||
</.title>
|
||||
|
||||
<.input
|
||||
autofocus
|
||||
field={f[:description]}
|
||||
label="Description"
|
||||
class="focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-900 dark:text-gray-300 block w-7/12 rounded-md sm:text-sm border-gray-300 dark:border-gray-500 w-full p-2 mt-2"
|
||||
placeholder="e.g. Signup"
|
||||
value={@token_description}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<.input
|
||||
autofocus
|
||||
field={f[:description]}
|
||||
label="Description"
|
||||
placeholder="e.g. Your Plugin Name"
|
||||
value={@token_description}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<.input_with_clipboard
|
||||
id="token-clipboard"
|
||||
name="token_clipboard"
|
||||
label="Plugin Token"
|
||||
value={@token.raw}
|
||||
onfocus="this.value = this.value;"
|
||||
class="focus:ring-indigo-500 focus:border-indigo-500 bg-gray-50 dark:bg-gray-850 dark:text-gray-300 block w-7/12 rounded-md sm:text-sm border-gray-300 dark:border-gray-500 w-full p-2 mt-2"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<.input_with_clipboard
|
||||
id="token-clipboard"
|
||||
name="token_clipboard"
|
||||
label="Plugin Token"
|
||||
value={@token.raw}
|
||||
onfocus="this.value = this.value;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="text-sm mt-2 text-gray-500 dark:text-gray-200">
|
||||
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
Once created, we will not be able to show the Plugin Token again.
|
||||
Please copy the Plugin Token now and store it in a secure place.
|
||||
<span :if={@token_description == "WordPress"}>
|
||||
You'll need to paste it in the settings area of the Plausible WordPress plugin.
|
||||
</span>
|
||||
</p>
|
||||
<div class="py-4 mt-8">
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
Add Plugin Token →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
<.button type="submit" class="w-full">
|
||||
Add Plugin Token
|
||||
</.button>
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -49,9 +49,9 @@ defmodule PlausibleWeb.Live.PropsSettings.Form do
|
||||
phx-submit="allow-prop"
|
||||
phx-click-away="cancel-allow-prop"
|
||||
>
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Add Property for <%= @domain %></h2>
|
||||
<.title>Add Property for <%= @domain %></.title>
|
||||
|
||||
<div class="py-2">
|
||||
<div class="mt-6">
|
||||
<.label for="prop_input">
|
||||
Property
|
||||
</.label>
|
||||
@ -91,16 +91,14 @@ defmodule PlausibleWeb.Live.PropsSettings.Form do
|
||||
</.error>
|
||||
</div>
|
||||
|
||||
<div class="py-4">
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
Add Property →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
Add Property →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
|
||||
<button
|
||||
:if={@prop_key_options_count > 0}
|
||||
title="Use this to add any existing properties from your past events into your settings. This allows you to set up properties without having to manually enter each item."
|
||||
class="mt-2 text-sm hover:underline text-indigo-600 dark:text-indigo-400 text-left"
|
||||
class="mt-4 text-sm hover:underline text-indigo-600 dark:text-indigo-400 text-left"
|
||||
phx-click="allow-existing-props"
|
||||
>
|
||||
Already sending custom properties? Click to add <%= @prop_key_options_count %> existing properties we found.
|
||||
|
@ -4,6 +4,7 @@ defmodule PlausibleWeb.Live.PropsSettings.List do
|
||||
"""
|
||||
use Phoenix.LiveComponent
|
||||
use Phoenix.HTML
|
||||
import PlausibleWeb.Components.Generic
|
||||
|
||||
attr(:props, :list, required: true)
|
||||
attr(:domain, :string, required: true)
|
||||
@ -12,66 +13,33 @@ defmodule PlausibleWeb.Live.PropsSettings.List do
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<div class="border-t border-gray-200 pt-4 sm:flex sm:items-center sm:justify-between">
|
||||
<form id="filter-form" phx-change="filter">
|
||||
<div class="text-gray-800 text-sm inline-flex items-center">
|
||||
<div class="relative mt-2 rounded-md shadow-sm flex">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<Heroicons.magnifying_glass class="feather mr-1 dark:text-gray-300" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="filter-text"
|
||||
id="filter-text"
|
||||
class="pl-8 shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800"
|
||||
placeholder="Search Properties"
|
||||
value={@filter_text}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Heroicons.backspace
|
||||
:if={String.trim(@filter_text) != ""}
|
||||
class="feather ml-2 cursor-pointer hover:text-red-500 dark:text-gray-300 dark:hover:text-red-500 mt-2"
|
||||
phx-click="reset-filter-text"
|
||||
id="reset-filter"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4 flex sm:ml-4 sm:mt-0">
|
||||
<PlausibleWeb.Components.Generic.button phx-click="add-prop">
|
||||
+ Add Property
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</div>
|
||||
<.filter_bar filter_text={@filter_text} placeholder="Search Properties">
|
||||
<.button phx-click="add-prop" mt?={false}>
|
||||
Add Property
|
||||
</.button>
|
||||
</.filter_bar>
|
||||
<%= if is_list(@props) && length(@props) > 0 do %>
|
||||
<ul id="allowed-props" class="mt-12 divide-gray-200 divide-y dark:divide-gray-600">
|
||||
<li :for={{prop, index} <- Enum.with_index(@props)} id={"prop-#{index}"} class="flex py-4">
|
||||
<span class="flex-1 truncate font-medium text-sm text-gray-800 dark:text-gray-200">
|
||||
<%= prop %>
|
||||
</span>
|
||||
<button
|
||||
id={"disallow-prop-#{prop}"}
|
||||
data-confirm={delete_confirmation_text(prop)}
|
||||
phx-click="disallow-prop"
|
||||
phx-value-prop={prop}
|
||||
class="w-4 h-4 text-red-600 hover:text-red-700"
|
||||
aria-label={"Remove #{prop} property"}
|
||||
>
|
||||
<Heroicons.trash class="feather feather-sm" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<.table id="allowed-props" rows={Enum.with_index(@props)}>
|
||||
<:tbody :let={{prop, index}}>
|
||||
<.td id={"prop-#{index}"}><span class="font-medium"><%= prop %></span></.td>
|
||||
<.td actions>
|
||||
<.delete_button
|
||||
id={"disallow-prop-#{prop}"}
|
||||
data-confirm={delete_confirmation_text(prop)}
|
||||
phx-click="disallow-prop"
|
||||
phx-value-prop={prop}
|
||||
aria-label={"Remove #{prop} property"}
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
<% else %>
|
||||
<p class="text-sm text-gray-800 dark:text-gray-200 mt-12 mb-8 text-center">
|
||||
<p class="mt-12 mb-8 text-center text-sm">
|
||||
<span :if={String.trim(@filter_text) != ""}>
|
||||
No properties found for this site. Please refine or
|
||||
<a
|
||||
class="text-indigo-500 cursor-pointer underline"
|
||||
phx-click="reset-filter-text"
|
||||
id="reset-filter-hint"
|
||||
>
|
||||
<.styled_link phx-click="reset-filter-text" id="reset-filter-hint">
|
||||
reset your search.
|
||||
</a>
|
||||
</.styled_link>
|
||||
</span>
|
||||
<span :if={String.trim(@filter_text) == "" && Enum.empty?(@props)}>
|
||||
No properties configured for this site.
|
||||
|
@ -9,6 +9,7 @@ defmodule PlausibleWeb.Live.Shields.CountryRules do
|
||||
alias PlausibleWeb.Live.Components.Modal
|
||||
alias Plausible.Shields
|
||||
alias Plausible.Shield
|
||||
import PlausibleWeb.Components.Generic
|
||||
|
||||
def update(assigns, socket) do
|
||||
socket =
|
||||
@ -28,145 +29,112 @@ defmodule PlausibleWeb.Live.Shields.CountryRules do
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<section class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden">
|
||||
<div class="py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
Country Block List
|
||||
</h2>
|
||||
<p class="mt-1 mb-4 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Reject incoming traffic from specific countries
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="countries" />
|
||||
</header>
|
||||
<div class="border-t border-gray-200 pt-4 grid">
|
||||
<div
|
||||
<div>
|
||||
<.settings_tiles>
|
||||
<.tile docs="excluding">
|
||||
<:title>Country Block List</:title>
|
||||
<:subtitle>Reject incoming traffic from specific countries</:subtitle>
|
||||
<.filter_bar
|
||||
:if={@country_rules_count < Shields.maximum_country_rules()}
|
||||
class="mt-4 sm:ml-4 sm:mt-0 justify-self-end"
|
||||
filtering_enabled?={false}
|
||||
>
|
||||
<PlausibleWeb.Components.Generic.button
|
||||
<.button
|
||||
id="add-country-rule"
|
||||
x-data
|
||||
x-on:click={Modal.JS.open("country-rule-form-modal")}
|
||||
mt?={false}
|
||||
>
|
||||
+ Add Country
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
<PlausibleWeb.Components.Generic.notice
|
||||
Add Country
|
||||
</.button>
|
||||
</.filter_bar>
|
||||
|
||||
<.notice
|
||||
:if={@country_rules_count >= Shields.maximum_country_rules()}
|
||||
class="mt-4"
|
||||
title="Maximum number of countries reached"
|
||||
theme={:gray}
|
||||
>
|
||||
<p>
|
||||
You've reached the maximum number of countries you can block (<%= Shields.maximum_country_rules() %>). Please remove one before adding another.
|
||||
</p>
|
||||
</PlausibleWeb.Components.Generic.notice>
|
||||
</div>
|
||||
</.notice>
|
||||
|
||||
<.live_component :let={modal_unique_id} module={Modal} id="country-rule-form-modal">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@form}
|
||||
phx-submit="save-country-rule"
|
||||
phx-target={@myself}
|
||||
class="max-w-md w-full mx-auto bg-white dark:bg-gray-800"
|
||||
>
|
||||
<h2 class="text-xl font-black dark:text-gray-100 mb-8">Add Country to Block List</h2>
|
||||
<p :if={Enum.empty?(@country_rules)} class="mt-12 mb-8 text-center text-sm">
|
||||
No Country Rules configured for this site.
|
||||
</p>
|
||||
|
||||
<.live_component
|
||||
submit_name="country_rule[country_code]"
|
||||
submit_value={f[:country_code].value}
|
||||
display_value=""
|
||||
module={PlausibleWeb.Live.Components.ComboBox}
|
||||
suggest_fun={&PlausibleWeb.Live.Components.ComboBox.StaticSearch.suggest/2}
|
||||
id={"#{f[:country_code].id}-#{modal_unique_id}"}
|
||||
suggestions_limit={300}
|
||||
options={options(@country_rules)}
|
||||
/>
|
||||
<.table :if={not Enum.empty?(@country_rules)} rows={@country_rules}>
|
||||
<:thead>
|
||||
<.th>Country</.th>
|
||||
<.th hide_on_mobile>Status</.th>
|
||||
<.th invisible>Actions</.th>
|
||||
</:thead>
|
||||
<:tbody :let={rule}>
|
||||
<% country = Location.Country.get_country(rule.country_code) %>
|
||||
<.td>
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
id={"country-#{rule.id}"}
|
||||
class="mr-4 cursor-help"
|
||||
title={"Added at #{format_added_at(rule.inserted_at, @site.timezone)} by #{rule.added_by}"}
|
||||
>
|
||||
<%= country.flag %> <%= country.name %>
|
||||
</span>
|
||||
</div>
|
||||
</.td>
|
||||
<.td hide_on_mobile>
|
||||
<span :if={rule.action == :deny}>
|
||||
Blocked
|
||||
</span>
|
||||
<span :if={rule.action == :allow}>
|
||||
Allowed
|
||||
</span>
|
||||
</.td>
|
||||
<.td actions>
|
||||
<.delete_button
|
||||
id={"remove-country-rule-#{rule.id}"}
|
||||
phx-target={@myself}
|
||||
phx-click="remove-country-rule"
|
||||
phx-value-rule-id={rule.id}
|
||||
data-confirm="Are you sure you want to revoke this rule?"
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
|
||||
<p class="text-sm mt-2 text-gray-500 dark:text-gray-200">
|
||||
Once added, we will start rejecting traffic from this country within a few minutes.
|
||||
</p>
|
||||
<div class="py-4 mt-8">
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
Add Country →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</.form>
|
||||
</.live_component>
|
||||
<.live_component :let={modal_unique_id} module={Modal} id="country-rule-form-modal">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@form}
|
||||
phx-submit="save-country-rule"
|
||||
phx-target={@myself}
|
||||
class="max-w-md w-full mx-auto bg-white dark:bg-gray-800"
|
||||
>
|
||||
<.title>Add Country to Block List</.title>
|
||||
|
||||
<p
|
||||
:if={Enum.empty?(@country_rules)}
|
||||
class="text-sm text-gray-800 dark:text-gray-200 mt-12 mb-8 text-center"
|
||||
>
|
||||
No Country Rules configured for this Site.
|
||||
</p>
|
||||
<div
|
||||
:if={not Enum.empty?(@country_rules)}
|
||||
class="mt-8 overflow-visible border-b border-gray-200 shadow dark:border-gray-900 sm:rounded-lg"
|
||||
>
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-900">
|
||||
<thead class="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
Country
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
<span class="sr-only">Remove</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for rule <- @country_rules do %>
|
||||
<% country = Location.Country.get_country(rule.country_code) %>
|
||||
<tr class="text-gray-900 dark:text-gray-100">
|
||||
<td class="px-6 py-4 text-sm font-medium">
|
||||
<PlausibleWeb.Components.Generic.tooltip>
|
||||
<:tooltip_content>
|
||||
Added at <%= format_added_at(rule.inserted_at, @site.timezone) %> by <%= rule.added_by %>
|
||||
</:tooltip_content>
|
||||
<span id={"country-#{rule.id}"} class="mr-4 cursor-help">
|
||||
<%= country.flag %> <%= country.name %>
|
||||
</span>
|
||||
</PlausibleWeb.Components.Generic.tooltip>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
<span :if={rule.action == :deny}>
|
||||
Blocked
|
||||
</span>
|
||||
<span :if={rule.action == :allow}>
|
||||
Allowed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm font-medium text-right">
|
||||
<button
|
||||
id={"remove-country-rule-#{rule.id}"}
|
||||
phx-target={@myself}
|
||||
phx-click="remove-country-rule"
|
||||
phx-value-rule-id={rule.id}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm="Are you sure you want to revoke this rule?"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<.live_component
|
||||
class="mt-4"
|
||||
submit_name="country_rule[country_code]"
|
||||
submit_value={f[:country_code].value}
|
||||
display_value=""
|
||||
module={PlausibleWeb.Live.Components.ComboBox}
|
||||
suggest_fun={&PlausibleWeb.Live.Components.ComboBox.StaticSearch.suggest/2}
|
||||
id={"#{f[:country_code].id}-#{modal_unique_id}"}
|
||||
suggestions_limit={300}
|
||||
options={options(@country_rules)}
|
||||
/>
|
||||
|
||||
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
Once added, we will start rejecting traffic from this country within a few minutes.
|
||||
</p>
|
||||
<.button type="submit" class="w-full">
|
||||
Add Country
|
||||
</.button>
|
||||
</.form>
|
||||
</.live_component>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -11,6 +11,7 @@ defmodule PlausibleWeb.Live.Shields.HostnameRules do
|
||||
alias Plausible.Shield
|
||||
|
||||
import PlausibleWeb.ErrorHelpers
|
||||
import PlausibleWeb.Components.Generic
|
||||
|
||||
def update(assigns, socket) do
|
||||
socket =
|
||||
@ -33,170 +34,132 @@ defmodule PlausibleWeb.Live.Shields.HostnameRules do
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<section class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden">
|
||||
<div class="py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
Hostnames Allow List
|
||||
</h2>
|
||||
<p class="mt-1 mb-4 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Accept incoming traffic only from familiar hostnames.
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="excluding#exclude-visits-by-hostname" />
|
||||
</header>
|
||||
<div class="border-t border-gray-200 pt-4 grid">
|
||||
<div
|
||||
<div>
|
||||
<.settings_tiles>
|
||||
<.tile docs="excluding#exclude-visits-by-hostname">
|
||||
<:title>Hostnames Allow List</:title>
|
||||
<:subtitle>Accept incoming traffic only from familiar hostnames</:subtitle>
|
||||
<.filter_bar
|
||||
:if={@hostname_rules_count < Shields.maximum_hostname_rules()}
|
||||
class="mt-4 sm:ml-4 sm:mt-0 justify-self-end"
|
||||
filtering_enabled?={false}
|
||||
>
|
||||
<PlausibleWeb.Components.Generic.button
|
||||
<.button
|
||||
id="add-hostname-rule"
|
||||
x-data
|
||||
x-on:click={Modal.JS.open("hostname-rule-form-modal")}
|
||||
mt?={false}
|
||||
>
|
||||
+ Add Hostname
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
<PlausibleWeb.Components.Generic.notice
|
||||
Add Hostname
|
||||
</.button>
|
||||
</.filter_bar>
|
||||
|
||||
<.notice
|
||||
:if={@hostname_rules_count >= Shields.maximum_hostname_rules()}
|
||||
class="mt-4"
|
||||
title="Maximum number of hostnames reached"
|
||||
theme={:gray}
|
||||
>
|
||||
<p>
|
||||
You've reached the maximum number of hostnames you can block (<%= Shields.maximum_hostname_rules() %>). Please remove one before adding another.
|
||||
</p>
|
||||
</PlausibleWeb.Components.Generic.notice>
|
||||
</div>
|
||||
</.notice>
|
||||
|
||||
<.live_component :let={modal_unique_id} module={Modal} id="hostname-rule-form-modal">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@form}
|
||||
phx-submit="save-hostname-rule"
|
||||
phx-target={@myself}
|
||||
class="max-w-md w-full mx-auto bg-white dark:bg-gray-800"
|
||||
>
|
||||
<h2 class="text-xl font-black dark:text-gray-100 mb-8">Add Hostname to Allow List</h2>
|
||||
<p :if={Enum.empty?(@hostname_rules)} class="mt-12 mb-8 text-center text-sm">
|
||||
No Hostname Rules configured for this site.
|
||||
<strong>
|
||||
Traffic from all hostnames is currently accepted.
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
<.live_component
|
||||
submit_name="hostname_rule[hostname]"
|
||||
submit_value={f[:hostname].value}
|
||||
display_value={f[:hostname].value || ""}
|
||||
module={PlausibleWeb.Live.Components.ComboBox}
|
||||
suggest_fun={fn input, options -> suggest_hostnames(input, options, @site) end}
|
||||
id={"#{f[:hostname].id}-#{modal_unique_id}"}
|
||||
creatable
|
||||
/>
|
||||
<.table :if={not Enum.empty?(@hostname_rules)} rows={@hostname_rules}>
|
||||
<:thead>
|
||||
<.th>Hostname</.th>
|
||||
<.th hide_on_mobile>Status</.th>
|
||||
<.th invisible>Actions</.th>
|
||||
</:thead>
|
||||
<:tbody :let={rule}>
|
||||
<.td>
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
id={"hostname-#{rule.id}"}
|
||||
class="mr-4 cursor-help text-ellipsis truncate max-w-xs"
|
||||
title={"Added at #{format_added_at(rule.inserted_at, @site.timezone)} by #{rule.added_by}"}
|
||||
>
|
||||
<%= rule.hostname %>
|
||||
</span>
|
||||
</div>
|
||||
</.td>
|
||||
<.td hide_on_mobile>
|
||||
<div class="flex items-center">
|
||||
<span :if={rule.action == :deny}>
|
||||
Blocked
|
||||
</span>
|
||||
<span :if={rule.action == :allow}>
|
||||
Allowed
|
||||
</span>
|
||||
<span
|
||||
:if={@redundant_rules[rule.id]}
|
||||
title={"This rule might be redundant because the following rules may match first:\n\n#{Enum.join(@redundant_rules[rule.id], "\n")}"}
|
||||
class="pl-4 cursor-help"
|
||||
>
|
||||
<Heroicons.exclamation_triangle class="h-5 w-5 text-red-800" />
|
||||
</span>
|
||||
</div>
|
||||
</.td>
|
||||
<.td actions>
|
||||
<.delete_button
|
||||
id={"remove-hostname-rule-#{rule.id}"}
|
||||
phx-target={@myself}
|
||||
phx-click="remove-hostname-rule"
|
||||
phx-value-rule-id={rule.id}
|
||||
data-confirm="Are you sure you want to revoke this rule?"
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
|
||||
<%= error_tag(f, :hostname) %>
|
||||
<.live_component :let={modal_unique_id} module={Modal} id="hostname-rule-form-modal">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@form}
|
||||
phx-submit="save-hostname-rule"
|
||||
phx-target={@myself}
|
||||
class="max-w-md w-full mx-auto bg-white dark:bg-gray-800"
|
||||
>
|
||||
<.title>Add Hostname to Allow List</.title>
|
||||
|
||||
<p class="text-sm mt-2 text-gray-500 dark:text-gray-200">
|
||||
You can use a wildcard (<code>*</code>) to match multiple hostnames. For example,
|
||||
<code>*<%= @site.domain %></code>
|
||||
will only record traffic on your main domain and all of its subdomains.<br /><br />
|
||||
<.live_component
|
||||
class="mt-8"
|
||||
submit_name="hostname_rule[hostname]"
|
||||
submit_value={f[:hostname].value}
|
||||
display_value={f[:hostname].value || ""}
|
||||
module={PlausibleWeb.Live.Components.ComboBox}
|
||||
suggest_fun={fn input, options -> suggest_hostnames(input, options, @site) end}
|
||||
id={"#{f[:hostname].id}-#{modal_unique_id}"}
|
||||
creatable
|
||||
/>
|
||||
|
||||
<%= if @hostname_rules_count >= 1 do %>
|
||||
Once added, we will start accepting traffic from this hostname within a few minutes.
|
||||
<% else %>
|
||||
NB: Once added, we will start rejecting traffic from non-matching hostnames within a few minutes.
|
||||
<% end %>
|
||||
</p>
|
||||
<div class="py-4 mt-8">
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
Add Hostname →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</.form>
|
||||
</.live_component>
|
||||
<%= error_tag(f, :hostname) %>
|
||||
|
||||
<p
|
||||
:if={Enum.empty?(@hostname_rules)}
|
||||
class="text-sm text-gray-800 dark:text-gray-200 mt-12 mb-8 text-center"
|
||||
>
|
||||
No Hostname Rules configured for this Site.<br /><br />
|
||||
<strong>
|
||||
Traffic from all hostnames is currently accepted.
|
||||
</strong>
|
||||
</p>
|
||||
<div
|
||||
:if={not Enum.empty?(@hostname_rules)}
|
||||
class="mt-8 overflow-visible border-b border-gray-200 shadow dark:border-gray-900 sm:rounded-lg"
|
||||
>
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-900">
|
||||
<thead class="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
hostname
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
<span class="sr-only">Remove</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for rule <- @hostname_rules do %>
|
||||
<tr class="text-gray-900 dark:text-gray-100">
|
||||
<td class="px-6 py-4 text-sm font-medium">
|
||||
<PlausibleWeb.Components.Generic.tooltip>
|
||||
<:tooltip_content>
|
||||
Added at <%= format_added_at(rule.inserted_at, @site.timezone) %> by <%= rule.added_by %>
|
||||
</:tooltip_content>
|
||||
<div
|
||||
id={"hostname-#{rule.id}"}
|
||||
class="mr-4 cursor-help text-ellipsis truncate max-w-xs"
|
||||
>
|
||||
<%= rule.hostname %>
|
||||
</div>
|
||||
</PlausibleWeb.Components.Generic.tooltip>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
<div class="flex items-center">
|
||||
<span :if={rule.action == :deny}>
|
||||
Blocked
|
||||
</span>
|
||||
<span :if={rule.action == :allow} class="text-green-500">
|
||||
Allowed
|
||||
</span>
|
||||
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
You can use a wildcard (<code>*</code>) to match multiple hostnames. For example,
|
||||
<code>*<%= @site.domain %></code>
|
||||
will only record traffic on your main domain and all of its subdomains.<br /><br />
|
||||
|
||||
<span
|
||||
:if={@redundant_rules[rule.id]}
|
||||
title={"This rule might be redundant because the following rules may match first:\n\n#{Enum.join(@redundant_rules[rule.id], "\n")}"}
|
||||
class="pl-4"
|
||||
>
|
||||
<Heroicons.exclamation_triangle class="h-4 w-4 text-red-500" />
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="px-6 py-4 text-sm font-medium text-right">
|
||||
<button
|
||||
id={"remove-hostname-rule-#{rule.id}"}
|
||||
phx-target={@myself}
|
||||
phx-click="remove-hostname-rule"
|
||||
phx-value-rule-id={rule.id}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm="Are you sure you want to revoke this rule?"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<%= if @hostname_rules_count >= 1 do %>
|
||||
Once added, we will start accepting traffic from this hostname within a few minutes.
|
||||
<% else %>
|
||||
NB: Once added, we will start rejecting traffic from non-matching hostnames within a few minutes.
|
||||
<% end %>
|
||||
</p>
|
||||
<.button type="submit" class="w-full">
|
||||
Add Hostname
|
||||
</.button>
|
||||
</.form>
|
||||
</.live_component>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -31,188 +31,141 @@ defmodule PlausibleWeb.Live.Shields.IPRules do
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<section class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden">
|
||||
<div class="py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
IP Block List
|
||||
</h2>
|
||||
<p class="mt-1 mb-4 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Reject incoming traffic from specific IP addresses
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="excluding" />
|
||||
</header>
|
||||
<div class="border-t border-gray-200 pt-4 grid">
|
||||
<div
|
||||
:if={@ip_rules_count < Shields.maximum_ip_rules()}
|
||||
class="mt-4 sm:ml-4 sm:mt-0 justify-self-end"
|
||||
>
|
||||
<PlausibleWeb.Components.Generic.button
|
||||
<div>
|
||||
<.settings_tiles>
|
||||
<.tile docs="excluding">
|
||||
<:title>IP Block List</:title>
|
||||
<:subtitle>Reject incoming traffic from specific IP addresses</:subtitle>
|
||||
<.filter_bar :if={@ip_rules_count < Shields.maximum_ip_rules()} filtering_enabled?={false}>
|
||||
<.button
|
||||
id="add-ip-rule"
|
||||
x-data
|
||||
x-on:click={Modal.JS.open("ip-rule-form-modal")}
|
||||
mt?={false}
|
||||
>
|
||||
+ Add IP Address
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
<PlausibleWeb.Components.Generic.notice
|
||||
Add IP Address
|
||||
</.button>
|
||||
</.filter_bar>
|
||||
|
||||
<.notice
|
||||
:if={@ip_rules_count >= Shields.maximum_ip_rules()}
|
||||
class="mt-4"
|
||||
title="Maximum number of addresses reached"
|
||||
theme={:gray}
|
||||
>
|
||||
<p>
|
||||
You've reached the maximum number of IP addresses you can block (<%= Shields.maximum_ip_rules() %>). Please remove one before adding another.
|
||||
</p>
|
||||
</PlausibleWeb.Components.Generic.notice>
|
||||
</div>
|
||||
</.notice>
|
||||
|
||||
<.live_component module={Modal} id="ip-rule-form-modal">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@form}
|
||||
phx-submit="save-ip-rule"
|
||||
phx-target={@myself}
|
||||
class="max-w-md w-full mx-auto bg-white dark:bg-gray-800"
|
||||
>
|
||||
<h2 class="text-xl font-black dark:text-gray-100 mb-8">Add IP to Block List</h2>
|
||||
<p :if={Enum.empty?(@ip_rules)} class="mt-12 mb-8 text-center text-sm">
|
||||
No IP Rules configured for this site.
|
||||
</p>
|
||||
|
||||
<.input
|
||||
autofocus
|
||||
field={f[:inet]}
|
||||
label="IP Address"
|
||||
class="focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-900 dark:text-gray-300 block w-7/12 rounded-md sm:text-sm border-gray-300 dark:border-gray-500 w-full p-2 mt-2"
|
||||
placeholder="e.g. 192.168.127.12"
|
||||
/>
|
||||
<.table :if={not Enum.empty?(@ip_rules)} rows={@ip_rules}>
|
||||
<:thead>
|
||||
<.th>IP Address</.th>
|
||||
<.th hide_on_mobile>Status</.th>
|
||||
<.th hide_on_mobile>Description</.th>
|
||||
<.th invisible>Actions</.th>
|
||||
</:thead>
|
||||
<:tbody :let={rule}>
|
||||
<.td max_width="max-w-40">
|
||||
<div class="flex items-center truncate">
|
||||
<span
|
||||
id={"inet-#{rule.id}"}
|
||||
class="mr-4 cursor-help"
|
||||
title={"Added at #{format_added_at(rule.inserted_at, @site.timezone)} by #{rule.added_by}"}
|
||||
>
|
||||
<%= rule.inet %>
|
||||
</span>
|
||||
|
||||
<div class="mt-4">
|
||||
<p
|
||||
:if={not ip_rule_present?(@ip_rules, @remote_ip)}
|
||||
class="text-sm text-gray-500 dark:text-gray-200 mb-4"
|
||||
>
|
||||
Your current IP address is: <span class="font-mono"><%= @remote_ip %></span>
|
||||
<br />
|
||||
<.styled_link phx-target={@myself} phx-click="prefill-own-ip-rule">
|
||||
Click here
|
||||
</.styled_link>
|
||||
to block your own traffic, or enter a custom address.
|
||||
<span
|
||||
:if={to_string(rule.inet) == @remote_ip}
|
||||
class="inline-flex items-center gap-x-1.5 rounded-md px-2 py-1 text-xs font-medium text-gray-700 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700"
|
||||
>
|
||||
<svg class="h-1.5 w-1.5 fill-green-400" viewBox="0 0 6 6" aria-hidden="true">
|
||||
<circle cx="3" cy="3" r="3" />
|
||||
</svg>
|
||||
YOU
|
||||
</span>
|
||||
</div>
|
||||
</.td>
|
||||
<.td hide_on_mobile>
|
||||
<span :if={rule.action == :deny}>
|
||||
Blocked
|
||||
</span>
|
||||
<span :if={rule.action == :allow}>
|
||||
Allowed
|
||||
</span>
|
||||
</.td>
|
||||
<.td hide_on_mobile truncate>
|
||||
<span :if={rule.description} title={rule.description}>
|
||||
<%= rule.description %>
|
||||
</span>
|
||||
<span :if={!rule.description} class="text-gray-400 dark:text-gray-600">
|
||||
--
|
||||
</span>
|
||||
</.td>
|
||||
<.td actions>
|
||||
<.delete_button
|
||||
id={"remove-ip-rule-#{rule.id}"}
|
||||
phx-target={@myself}
|
||||
phx-click="remove-ip-rule"
|
||||
phx-value-rule-id={rule.id}
|
||||
data-confirm="Are you sure you want to revoke this rule?"
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
|
||||
<.live_component module={Modal} id="ip-rule-form-modal">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@form}
|
||||
phx-submit="save-ip-rule"
|
||||
phx-target={@myself}
|
||||
class="max-w-md w-full mx-auto bg-white dark:bg-gray-800"
|
||||
>
|
||||
<.title>Add IP to Block List</.title>
|
||||
|
||||
<div class="mt-4">
|
||||
<p
|
||||
:if={not ip_rule_present?(@ip_rules, @remote_ip)}
|
||||
class="text-sm text-gray-500 dark:text-gray-400 mb-4"
|
||||
>
|
||||
Your current IP address is: <span class="font-mono"><%= @remote_ip %></span>.
|
||||
<.styled_link phx-target={@myself} phx-click="prefill-own-ip-rule">
|
||||
Click here
|
||||
</.styled_link>
|
||||
to block your own traffic, or enter a custom address below.
|
||||
</p>
|
||||
|
||||
<.input
|
||||
autofocus
|
||||
field={f[:inet]}
|
||||
label="IP Address"
|
||||
placeholder="e.g. 192.168.127.12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<.input
|
||||
field={f[:description]}
|
||||
label="Description (optional)"
|
||||
placeholder="e.g. The Office"
|
||||
/>
|
||||
|
||||
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
Once added, we will start rejecting traffic from this IP within a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<.input
|
||||
field={f[:description]}
|
||||
label="Description"
|
||||
class="focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-900 dark:text-gray-300 block w-7/12 rounded-md sm:text-sm border-gray-300 dark:border-gray-500 w-full p-2 mt-2"
|
||||
placeholder="e.g. The Office"
|
||||
/>
|
||||
|
||||
<p class="text-sm mt-2 text-gray-500 dark:text-gray-200">
|
||||
Once added, we will start rejecting traffic from this IP within a few minutes.
|
||||
</p>
|
||||
<div class="py-4 mt-8">
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
<.button type="submit" class="w-full">
|
||||
Add IP Address →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</.form>
|
||||
</.live_component>
|
||||
|
||||
<p
|
||||
:if={Enum.empty?(@ip_rules)}
|
||||
class="text-sm text-gray-800 dark:text-gray-200 mt-12 mb-8 text-center"
|
||||
>
|
||||
No IP Rules configured for this Site.
|
||||
</p>
|
||||
<div
|
||||
:if={not Enum.empty?(@ip_rules)}
|
||||
class="mt-8 overflow-visible border-b border-gray-200 shadow dark:border-gray-900 sm:rounded-lg"
|
||||
>
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-900">
|
||||
<thead class="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
IP Address
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100 md:block hidden"
|
||||
>
|
||||
Description
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
<span class="sr-only">Remove</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for rule <- @ip_rules do %>
|
||||
<tr class="text-gray-900 dark:text-gray-100">
|
||||
<td class="px-6 py-4 text-xs font-medium">
|
||||
<div class="flex items-center">
|
||||
<.tooltip>
|
||||
<:tooltip_content>
|
||||
Added at <%= format_added_at(rule.inserted_at, @site.timezone) %> by <%= rule.added_by %>
|
||||
</:tooltip_content>
|
||||
<span id={"inet-#{rule.id}"} class="font-mono mr-4 cursor-help">
|
||||
<%= rule.inet %>
|
||||
</span>
|
||||
</.tooltip>
|
||||
|
||||
<span
|
||||
:if={to_string(rule.inet) == @remote_ip}
|
||||
class="inline-flex items-center gap-x-1.5 rounded-md px-2 py-1 text-xs font-medium text-gray-700 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700"
|
||||
>
|
||||
<svg class="h-1.5 w-1.5 fill-green-400" viewBox="0 0 6 6" aria-hidden="true">
|
||||
<circle cx="3" cy="3" r="3" />
|
||||
</svg>
|
||||
YOU
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
<span :if={rule.action == :deny}>
|
||||
Blocked
|
||||
</span>
|
||||
<span :if={rule.action == :allow}>
|
||||
Allowed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm font-normal whitespace-nowrap truncate max-w-xs md:block hidden">
|
||||
<span :if={rule.description} title={rule.description}>
|
||||
<%= rule.description %>
|
||||
</span>
|
||||
<span :if={!rule.description} class="text-gray-400 dark:text-gray-600">
|
||||
--
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm font-medium text-right">
|
||||
<button
|
||||
id={"remove-ip-rule-#{rule.id}"}
|
||||
phx-target={@myself}
|
||||
phx-click="remove-ip-rule"
|
||||
phx-value-rule-id={rule.id}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm="Are you sure you want to revoke this rule?"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</.button>
|
||||
</.form>
|
||||
</.live_component>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -11,6 +11,7 @@ defmodule PlausibleWeb.Live.Shields.PageRules do
|
||||
alias Plausible.Shield
|
||||
|
||||
import PlausibleWeb.ErrorHelpers
|
||||
import PlausibleWeb.Components.Generic
|
||||
|
||||
def update(assigns, socket) do
|
||||
socket =
|
||||
@ -33,162 +34,122 @@ defmodule PlausibleWeb.Live.Shields.PageRules do
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<section class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden">
|
||||
<div class="py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
Pages Block List
|
||||
</h2>
|
||||
<p class="mt-1 mb-4 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Reject incoming traffic for specific pages
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="top-pages#block-traffic-from-specific-pages-or-sections" />
|
||||
</header>
|
||||
<div class="border-t border-gray-200 pt-4 grid">
|
||||
<div
|
||||
<div>
|
||||
<.settings_tiles>
|
||||
<.tile docs="top-pages#block-traffic-from-specific-pages-or-sections">
|
||||
<:title>Pages Block List</:title>
|
||||
<:subtitle>Reject incoming traffic for specific pages</:subtitle>
|
||||
<.filter_bar
|
||||
:if={@page_rules_count < Shields.maximum_page_rules()}
|
||||
class="mt-4 sm:ml-4 sm:mt-0 justify-self-end"
|
||||
filtering_enabled?={false}
|
||||
>
|
||||
<PlausibleWeb.Components.Generic.button
|
||||
<.button
|
||||
id="add-page-rule"
|
||||
x-data
|
||||
x-on:click={Modal.JS.open("page-rule-form-modal")}
|
||||
mt?={false}
|
||||
>
|
||||
+ Add Page
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
<PlausibleWeb.Components.Generic.notice
|
||||
Add Page
|
||||
</.button>
|
||||
</.filter_bar>
|
||||
|
||||
<.notice
|
||||
:if={@page_rules_count >= Shields.maximum_page_rules()}
|
||||
class="mt-4"
|
||||
title="Maximum number of pages reached"
|
||||
theme={:gray}
|
||||
>
|
||||
<p>
|
||||
You've reached the maximum number of pages you can block (<%= Shields.maximum_page_rules() %>). Please remove one before adding another.
|
||||
</p>
|
||||
</PlausibleWeb.Components.Generic.notice>
|
||||
</div>
|
||||
</.notice>
|
||||
|
||||
<.live_component :let={modal_unique_id} module={Modal} id="page-rule-form-modal">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@form}
|
||||
phx-submit="save-page-rule"
|
||||
phx-target={@myself}
|
||||
class="max-w-md w-full mx-auto bg-white dark:bg-gray-800"
|
||||
>
|
||||
<h2 class="text-xl font-black dark:text-gray-100 mb-8">Add Page to Block List</h2>
|
||||
<p :if={Enum.empty?(@page_rules)} class="mt-12 mb-8 text-center text-sm">
|
||||
No Page Rules configured for this site.
|
||||
</p>
|
||||
|
||||
<.live_component
|
||||
submit_name="page_rule[page_path]"
|
||||
submit_value={f[:page_path].value}
|
||||
display_value={f[:page_path].value || ""}
|
||||
module={PlausibleWeb.Live.Components.ComboBox}
|
||||
suggest_fun={fn input, options -> suggest_page_paths(input, options, @site) end}
|
||||
id={"#{f[:page_path].id}-#{modal_unique_id}"}
|
||||
creatable
|
||||
/>
|
||||
|
||||
<%= error_tag(f, :page_path) %>
|
||||
|
||||
<p class="text-sm mt-2 text-gray-500 dark:text-gray-200">
|
||||
You can use a wildcard (<code>*</code>) to match multiple pages. For example,
|
||||
<code>/blog/*</code>
|
||||
will match <code>/blog/post</code>.
|
||||
Once added, we will start rejecting traffic from this page within a few minutes.
|
||||
</p>
|
||||
<div class="py-4 mt-8">
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
Add Page →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</.form>
|
||||
</.live_component>
|
||||
|
||||
<p
|
||||
:if={Enum.empty?(@page_rules)}
|
||||
class="text-sm text-gray-800 dark:text-gray-200 mt-12 mb-8 text-center"
|
||||
>
|
||||
No Page Rules configured for this Site.
|
||||
</p>
|
||||
<div
|
||||
:if={not Enum.empty?(@page_rules)}
|
||||
class="mt-8 overflow-visible border-b border-gray-200 shadow dark:border-gray-900 sm:rounded-lg"
|
||||
>
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-900">
|
||||
<thead class="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
<.table :if={not Enum.empty?(@page_rules)} rows={@page_rules}>
|
||||
<:thead>
|
||||
<.th>Page</.th>
|
||||
<.th hide_on_mobile>Status</.th>
|
||||
<.th invisible>Actions</.th>
|
||||
</:thead>
|
||||
<:tbody :let={rule}>
|
||||
<.td max_width="max-w-40" truncate>
|
||||
<span
|
||||
id={"page-#{rule.id}"}
|
||||
class="mr-4 cursor-help text-ellipsis truncate max-w-xs"
|
||||
title={"Added at #{format_added_at(rule.inserted_at, @site.timezone)} by #{rule.added_by}"}
|
||||
>
|
||||
page
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
<span class="sr-only">Remove</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for rule <- @page_rules do %>
|
||||
<tr class="text-gray-900 dark:text-gray-100">
|
||||
<td class="px-6 py-4 text-sm font-medium">
|
||||
<PlausibleWeb.Components.Generic.tooltip>
|
||||
<:tooltip_content>
|
||||
Added at <%= format_added_at(rule.inserted_at, @site.timezone) %> by <%= rule.added_by %>
|
||||
</:tooltip_content>
|
||||
<div
|
||||
id={"page-#{rule.id}"}
|
||||
class="mr-4 cursor-help text-ellipsis truncate max-w-xs"
|
||||
>
|
||||
<%= rule.page_path %>
|
||||
</div>
|
||||
</PlausibleWeb.Components.Generic.tooltip>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
<div class="flex items-center">
|
||||
<span :if={rule.action == :deny}>
|
||||
Blocked
|
||||
</span>
|
||||
<span :if={rule.action == :allow}>
|
||||
Allowed
|
||||
</span>
|
||||
<%= rule.page_path %>
|
||||
</span>
|
||||
</.td>
|
||||
<.td hide_on_mobile>
|
||||
<div class="flex items-center">
|
||||
<span :if={rule.action == :deny}>
|
||||
Blocked
|
||||
</span>
|
||||
<span :if={rule.action == :allow}>
|
||||
Allowed
|
||||
</span>
|
||||
<span
|
||||
:if={@redundant_rules[rule.id]}
|
||||
title={"This rule might be redundant because the following rules may match first:\n\n#{Enum.join(@redundant_rules[rule.id], "\n")}"}
|
||||
class="pl-4 cursor-help"
|
||||
>
|
||||
<Heroicons.exclamation_triangle class="h-5 w-5 text-red-800" />
|
||||
</span>
|
||||
</div>
|
||||
</.td>
|
||||
<.td actions>
|
||||
<.delete_button
|
||||
id={"remove-page-rule-#{rule.id}"}
|
||||
phx-target={@myself}
|
||||
phx-click="remove-page-rule"
|
||||
phx-value-rule-id={rule.id}
|
||||
data-confirm="Are you sure you want to revoke this rule?"
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
|
||||
<span
|
||||
:if={@redundant_rules[rule.id]}
|
||||
title={"This rule might be redundant because the following rules may match first:\n\n#{Enum.join(@redundant_rules[rule.id], "\n")}"}
|
||||
class="pl-4"
|
||||
>
|
||||
<Heroicons.exclamation_triangle class="h-4 w-4 text-red-500" />
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<.live_component :let={modal_unique_id} module={Modal} id="page-rule-form-modal">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@form}
|
||||
phx-submit="save-page-rule"
|
||||
phx-target={@myself}
|
||||
class="max-w-md w-full mx-auto bg-white dark:bg-gray-800"
|
||||
>
|
||||
<.title>Add Page to Block List</.title>
|
||||
|
||||
<td class="px-6 py-4 text-sm font-medium text-right">
|
||||
<button
|
||||
id={"remove-page-rule-#{rule.id}"}
|
||||
phx-target={@myself}
|
||||
phx-click="remove-page-rule"
|
||||
phx-value-rule-id={rule.id}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm="Are you sure you want to revoke this rule?"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<.live_component
|
||||
class="mt-4"
|
||||
submit_name="page_rule[page_path]"
|
||||
submit_value={f[:page_path].value}
|
||||
display_value={f[:page_path].value || ""}
|
||||
module={PlausibleWeb.Live.Components.ComboBox}
|
||||
suggest_fun={fn input, options -> suggest_page_paths(input, options, @site) end}
|
||||
id={"#{f[:page_path].id}-#{modal_unique_id}"}
|
||||
creatable
|
||||
/>
|
||||
|
||||
<%= error_tag(f, :page_path) %>
|
||||
|
||||
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
You can use a wildcard (<code>*</code>) to match multiple pages. For example,
|
||||
<code>/blog/*</code>
|
||||
will match <code>/blog/post</code>.
|
||||
Once added, we will start rejecting traffic from this page within a few minutes.
|
||||
</p>
|
||||
<.button type="submit" class="w-full">
|
||||
Add Page
|
||||
</.button>
|
||||
</.form>
|
||||
</.live_component>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -1,19 +1,18 @@
|
||||
<a
|
||||
href={@this_tab && "/" <> URI.encode_www_form(@site.domain) <> "/settings/" <> @this_tab}
|
||||
class={[
|
||||
"flex items-center px-3 py-2 text-sm leading-5 font-medium rounded-md outline-none focus:outline-none transition ease-in-out duration-150 cursor-default",
|
||||
"text-sm flex items-center px-2 py-2 leading-5 font-medium rounded-md outline-none focus:outline-none transition ease-in-out duration-150",
|
||||
is_current_tab(@conn, @this_tab) &&
|
||||
"cursor-default text-gray-900 dark:text-gray-100 bg-gray-100 dark:bg-gray-900 hover:text-gray-900 hover:bg-gray-100 focus:bg-gray-200 dark:focus:bg-gray-800",
|
||||
"text-gray-900 dark:text-gray-100 bg-gray-100 font-semibold dark:bg-gray-900 hover:text-gray-900 focus:bg-gray-200 dark:focus:bg-gray-800",
|
||||
@this_tab && not is_current_tab(@conn, @this_tab) &&
|
||||
"cursor-pointer text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800 focus:text-gray-900 focus:bg-gray-50 dark:focus:text-gray-100 dark:focus:bg-gray-800",
|
||||
!@this_tab && "text-gray-600 dark:text-gray-400",
|
||||
@submenu? && "text-xs"
|
||||
"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 focus:text-gray-900 focus:bg-gray-50 dark:focus:text-gray-100 dark:focus:bg-gray-800",
|
||||
!@this_tab && "text-gray-600 dark:text-gray-400"
|
||||
]}
|
||||
>
|
||||
<PlausibleWeb.Components.Generic.dynamic_icon
|
||||
:if={not @submenu? && @icon}
|
||||
name={@icon}
|
||||
class="h-4 w-4 mr-2"
|
||||
class={["h-4 w-4 mr-2", is_current_tab(@conn, @this_tab) && "stroke-2"]}
|
||||
/>
|
||||
<%= @text %>
|
||||
<Heroicons.chevron_down
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="container pt-6">
|
||||
<%= link("← Back to Stats",
|
||||
to: "/#{URI.encode_www_form(@site.domain)}",
|
||||
class: "text-sm text-indigo-600 font-bold"
|
||||
class: "text-indigo-600 font-bold text-sm"
|
||||
) %>
|
||||
<div class="pb-5 border-b border-gray-200 dark:border-gray-500">
|
||||
<h2 class="text-2xl font-bold leading-7 text-gray-900 dark:text-gray-100 sm:text-3xl sm:leading-9 sm:truncate">
|
||||
@ -15,7 +15,7 @@
|
||||
<% options = flat_settings_options(@conn) %>
|
||||
<%= select(f, :tab, options,
|
||||
class:
|
||||
"dark:bg-gray-800 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 outline-none focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100",
|
||||
"dark:bg-gray-800 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 outline-none focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md dark:text-gray-100",
|
||||
onchange:
|
||||
"location.href = '/" <>
|
||||
URI.encode_www_form(@site.domain) <> "/settings/' + event.target.value",
|
||||
@ -43,7 +43,7 @@
|
||||
conn: @conn,
|
||||
submenu?: false
|
||||
) %>
|
||||
<div class="ml-8">
|
||||
<div class="ml-6">
|
||||
<%= for %{key: key, value: val} <- value do %>
|
||||
<%= render("_settings_tab.html",
|
||||
icon: nil,
|
||||
|
@ -6,7 +6,8 @@
|
||||
<:title>Change your website domain</:title>
|
||||
|
||||
<:subtitle>
|
||||
Once you change your domain, <b>you must update Plausible Installation on your site within 72 hours to guarantee continuous tracking</b>.
|
||||
Once you change your domain, you <i>must</i>
|
||||
update Plausible Installation on your site within 72 hours to guarantee continuous tracking.
|
||||
<br /><br />If you're using the API, please also make sure to update your API credentials. Visit our
|
||||
<.styled_link new_tab href="https://plausible.io/docs/change-domain-name/">
|
||||
documentation
|
||||
@ -26,17 +27,15 @@
|
||||
</:footer>
|
||||
|
||||
<%= form_for @changeset, Routes.site_path(@conn, :change_domain_submit, @site.domain, flow: PlausibleWeb.Flows.domain_change()), [], fn f -> %>
|
||||
<h2 class="text-xl font-black dark:text-gray-100"></h2>
|
||||
|
||||
<div class="my-6">
|
||||
<%= label(f, :domain, class: "block font-medium dark:text-gray-300") %>
|
||||
<%= label(f, :domain, class: "text-sm block font-medium dark:text-gray-300") %>
|
||||
<p class="text-gray-500 dark:text-gray-400 mt-1 text-sm">
|
||||
Just the naked domain or subdomain without 'www', 'https' etc.
|
||||
</p>
|
||||
<div class="mt-2 flex rounded-md shadow-sm">
|
||||
<%= text_input(f, :domain,
|
||||
class:
|
||||
"focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 flex-1 block w-full px-3 py-2 rounded-none rounded-r-md border-gray-300 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300",
|
||||
"text-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 flex-1 block w-full px-3 py-2 rounded-none rounded-r-md border-gray-300 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300",
|
||||
placeholder: "example.com"
|
||||
) %>
|
||||
</div>
|
||||
|
@ -1,23 +1,16 @@
|
||||
<div class="max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded p-6 mb-4 mt-8">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Import from CSV files</h2>
|
||||
|
||||
<div class="my-3 space-y-1.5 text-sm text-gray-400">
|
||||
<p>
|
||||
Please ensure each file follows
|
||||
<.styled_link href="https://plausible.io/docs/csv-import">
|
||||
our CSV format guidelines.
|
||||
</.styled_link>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can upload multiple files simultaneously by either selecting them in the file dialog or dragging and dropping them into the designated area.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PlausibleWeb.Components.Generic.focus_box>
|
||||
<:title>Import from CSV files</:title>
|
||||
<:subtitle>
|
||||
Please ensure each file follows
|
||||
<.styled_link href="https://plausible.io/docs/csv-import">
|
||||
our CSV format guidelines.
|
||||
</.styled_link>
|
||||
You can upload multiple files simultaneously by either selecting them in the file dialog or dragging and dropping them into the designated area.
|
||||
</:subtitle>
|
||||
<%= live_render(@conn, PlausibleWeb.Live.CSVImport,
|
||||
session: %{
|
||||
"site_id" => @site.id,
|
||||
"storage" => on_ee(do: "s3", else: "local")
|
||||
}
|
||||
) %>
|
||||
</div>
|
||||
</PlausibleWeb.Components.Generic.focus_box>
|
||||
|
@ -62,13 +62,13 @@
|
||||
) %>
|
||||
<div class="ml-3 flex flex-col">
|
||||
<span
|
||||
class="text-gray-900 dark:text-gray-100 block font-medium"
|
||||
class="text-gray-900 dark:text-gray-100 block text-sm font-medium"
|
||||
x-class="{'text-indigo-900 dark:text-white': selectedOption === 'admin', 'text-gray-900 dark:text-gray-100': selectedOption !== 'admin'}"
|
||||
>
|
||||
Admin
|
||||
</span>
|
||||
<span
|
||||
class="text-gray-500 dark:text-gray-200 block"
|
||||
class="text-gray-500 dark:text-gray-400 text-sm block"
|
||||
x-class="{'text-indigo-700 dark:text-gray-100': selectedOption === 'admin', 'text-gray-500 dark:text-gray-200': selectedOption !== 'admin'}"
|
||||
>
|
||||
Can view stats, change site settings and invite other members
|
||||
@ -88,13 +88,13 @@
|
||||
) %>
|
||||
<div class="ml-3 flex flex-col">
|
||||
<span
|
||||
class="text-gray-900 dark:text-gray-100 block font-medium"
|
||||
class="text-gray-900 dark:text-gray-100 text-sm block font-medium"
|
||||
x-class="{'text-indigo-900 dark:text-white': selectedOption === 'viewer', 'text-gray-900 dark:text-gray-100': selectedOption !== 'viewer'}"
|
||||
>
|
||||
Viewer
|
||||
</span>
|
||||
<span
|
||||
class="text-gray-500 dark:text-gray-200 block"
|
||||
class="text-gray-500 dark:text-gray-400 text-sm block"
|
||||
x-class="{'text-indigo-700 dark:text-gray-100': selectedOption === 'viewer', 'text-gray-500 dark:text-gray-200': selectedOption !== 'viewer'}"
|
||||
>
|
||||
Can view stats but cannot access settings or invite members
|
||||
|
@ -43,8 +43,6 @@
|
||||
<%= error_tag(f, :email) %>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<%= submit("Request transfer", class: "button w-full") %>
|
||||
</div>
|
||||
<.button type="submit" class="w-full" mt?={false}>Request transfer</.button>
|
||||
<% end %>
|
||||
</PlausibleWeb.Components.Generic.focus_box>
|
||||
|
@ -1,22 +0,0 @@
|
||||
<%= form_for @changeset, "/sites/#{URI.encode_www_form(@site.domain)}/shared-links", [class: "max-w-md w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
|
||||
<h2 class="text-xl font-black dark:text-gray-100">New shared link</h2>
|
||||
<div class="my-4">
|
||||
<%= label f, :name, "Name", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
||||
<div class="mt-1">
|
||||
<%= text_input f, :name, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-900 dark:border-gray-500 dark:text-gray-300 dark:focus:bg-gray-800 dark:focus:border-gray-500", required: "required", autocomplete: "off" %>
|
||||
<%= error_tag f, :name %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4">
|
||||
<%= label f, :password, "Password (optional)", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
||||
<div class="mt-1">
|
||||
<%= password_input f, :password, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-900 dark:border-gray-500 dark:text-gray-300 dark:focus:bg-gray-800 dark:focus:border-gray-500", autocomplete: "new-password" %>
|
||||
<%= error_tag f, :password %>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-200">
|
||||
Password protection is optional. Please make sure you save it in a secure place. Once the link is created, we cannot reveal the password.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= submit "Create shared link", class: "button mt-4 w-full" %>
|
||||
<% end %>
|
26
lib/plausible_web/templates/site/new_shared_link.html.heex
Normal file
26
lib/plausible_web/templates/site/new_shared_link.html.heex
Normal file
@ -0,0 +1,26 @@
|
||||
<.focus_box>
|
||||
<:title>New Shared Link</:title>
|
||||
<:subtitle>
|
||||
Password protection is optional. Please make sure you save it in a secure place. Once the link is created, we cannot reveal the password.
|
||||
</:subtitle>
|
||||
<%= form_for @changeset, "/sites/#{URI.encode_www_form(@site.domain)}/shared-links", [], fn f -> %>
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<PlausibleWeb.Live.Components.Form.input
|
||||
field={f[:name]}
|
||||
label="Name"
|
||||
required="required"
|
||||
autocomplete="off"
|
||||
mt?={false}
|
||||
/>
|
||||
<PlausibleWeb.Live.Components.Form.input
|
||||
field={f[:password]}
|
||||
label="Password (optional)"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
mt?={false}
|
||||
/>
|
||||
|
||||
<.button class="w-full mt-4" type="submit" mt?={false}>Create shared link</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
</.focus_box>
|
@ -1,47 +0,0 @@
|
||||
<div class="sm:rounded-md sm:overflow-hidden shadow">
|
||||
<div class="bg-white dark:bg-gray-800 py-6 px-4 space-y-6 sm:p-6">
|
||||
<div>
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Danger Zone</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">Destructive actions below can result in irrecoverable data loss. Be careful.</p>
|
||||
</div>
|
||||
<%= if @conn.assigns[:current_user_role] == :owner do %>
|
||||
<li class="py-4 flex items-center justify-between space-x-4">
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm leading-5 font-medium text-gray-900 dark:text-gray-100">
|
||||
Transfer Site Ownership
|
||||
</p>
|
||||
<p class="text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Transfer ownership of the site to a different account
|
||||
</p>
|
||||
</div>
|
||||
<%= link("Transfer #{@site.domain} ownership", to: Routes.membership_path(@conn, :transfer_ownership_form, @site.domain), class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150") %>
|
||||
</li>
|
||||
<div class="border-b border-gray-200 dark:border-gray-500"></div>
|
||||
<% end %>
|
||||
|
||||
<li class="py-4 flex items-center justify-between space-x-4">
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm leading-5 font-medium text-gray-900 dark:text-gray-100">
|
||||
Reset Stats
|
||||
</p>
|
||||
<p class="text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Reset all stats but keep the site configuration intact
|
||||
</p>
|
||||
</div>
|
||||
<%= link("Reset #{@site.domain} stats", to: "/#{URI.encode_www_form(@site.domain)}/stats", method: :delete, class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", data: [confirm: "Resetting the stats cannot be reversed. Are you sure?"]) %>
|
||||
</li>
|
||||
<div class="border-b border-gray-200 dark:border-gray-500"></div>
|
||||
|
||||
<li class="py-4 flex items-center justify-between space-x-4">
|
||||
<div class="flex max-w-md flex-col">
|
||||
<p class="text-sm leading-5 font-medium text-gray-900 dark:text-gray-100">
|
||||
Delete Site
|
||||
</p>
|
||||
<p class="text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Permanently remove all stats and the site configuration too
|
||||
</p>
|
||||
</div>
|
||||
<%= link "Delete #{@site.domain}", to: "/#{URI.encode_www_form(@site.domain)}", method: :delete, class: "inline-block px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-800 bg-red-100 dark:bg-red-200 hover:bg-red-50 dark:hover:bg-red-300 focus:outline-none focus:border-red-300 focus:ring active:bg-red-200 transition ease-in-out duration-150 sm:text-sm sm:leading-5", data: [confirm: "Deleting the site data cannot be reversed. Are you sure?"] %>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,42 @@
|
||||
<.notice title="Danger Zone" theme={:red}>
|
||||
Destructive actions below can result in irrecoverable data loss. Be careful.
|
||||
</.notice>
|
||||
|
||||
<.settings_tiles>
|
||||
<.tile>
|
||||
<:title>Transfer Site Ownership</:title>
|
||||
<:subtitle>Transfer ownership of the site to a different account</:subtitle>
|
||||
<.button_link
|
||||
href={Routes.membership_path(@conn, :transfer_ownership_form, @site.domain)}
|
||||
theme="danger"
|
||||
>
|
||||
Transfer <%= @site.domain %> ownership
|
||||
</.button_link>
|
||||
</.tile>
|
||||
|
||||
<.tile>
|
||||
<:title>Reset Stats</:title>
|
||||
<:subtitle>Reset all stats but keep the site configuration intact</:subtitle>
|
||||
<.button_link
|
||||
href={Routes.site_path(@conn, :reset_stats, @site.domain)}
|
||||
method="delete"
|
||||
data-confirm="Resetting the stats cannot be reversed. Are you sure?"
|
||||
theme="danger"
|
||||
>
|
||||
Reset <%= @site.domain %> stats
|
||||
</.button_link>
|
||||
</.tile>
|
||||
|
||||
<.tile>
|
||||
<:title>Delete Site</:title>
|
||||
<:subtitle>Permanently remove all stats and the site configuration too</:subtitle>
|
||||
<.button_link
|
||||
href={Routes.site_path(@conn, :delete_site, @site.domain)}
|
||||
theme="danger"
|
||||
method="delete"
|
||||
data-confirm="Deleting the site data cannot be reversed. Are you sure?"
|
||||
>
|
||||
Delete <%= @site.domain %>
|
||||
</.button_link>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
@ -1,218 +1,240 @@
|
||||
<div class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Email Reports</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Send weekly/monthly analytics reports to as many addresses as you wish
|
||||
</p>
|
||||
<.settings_tiles>
|
||||
<% email_reports = [
|
||||
weekly: %{
|
||||
report: @weekly_report,
|
||||
disable_route: :disable_weekly_report,
|
||||
enable_route: :enable_weekly_report,
|
||||
remove_route: :remove_weekly_report_recipient,
|
||||
add_route: :add_weekly_report_recipient,
|
||||
heading: "Weekly Email Reports",
|
||||
subtitle: "Send weekly analytics reports to as many addresses as you wish",
|
||||
toggle: "Send a weekly email report every Monday",
|
||||
add_label: "Add Weekly Report Recipient"
|
||||
},
|
||||
monthly: %{
|
||||
report: @monthly_report,
|
||||
disable_route: :disable_monthly_report,
|
||||
enable_route: :enable_monthly_report,
|
||||
remove_route: :remove_monthly_report_recipient,
|
||||
add_route: :add_monthly_report_recipient,
|
||||
heading: "Monthly Email Reports",
|
||||
subtitle: "Send monthly analytics reports to as many addresses as you wish",
|
||||
toggle: "Send a monthly email report on 1st of the month",
|
||||
add_label: "Add Monthly Report Recipient"
|
||||
}
|
||||
] %>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="email-reports" />
|
||||
</header>
|
||||
<.tile :for={{_type, meta} <- email_reports} docs="email-reports">
|
||||
<:title>
|
||||
<%= meta.heading %>
|
||||
</:title>
|
||||
<:subtitle>
|
||||
<%= meta.subtitle %>
|
||||
</:subtitle>
|
||||
|
||||
<div class="my-8 flex items-center">
|
||||
<%= if @weekly_report do %>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/weekly-report/disable", method: :post, class: "bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %>
|
||||
<span class="translate-x-5 inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-800 shadow transform transition ease-in-out duration-200">
|
||||
</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/weekly-report/enable", method: :post, class: "bg-gray-200 dark:bg-gray-700 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %>
|
||||
<span class="translate-x-0 inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-800 shadow transform transition ease-in-out duration-200">
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<span class="ml-2 dark:text-gray-100">Send a weekly email report every Monday</span>
|
||||
</div>
|
||||
<%= if @weekly_report do %>
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300 mt-6">
|
||||
<h4 class="font-bold my-2 dark:text-gray-100">Weekly report recipients</h4>
|
||||
<%= for recipient <- @weekly_report.recipients do %>
|
||||
<div class="p-2 pl-3 flex justify-between bg-gray-100 dark:bg-gray-900 rounded my-2 max-w-md">
|
||||
<span>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400 inline mr-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
<%= recipient %>
|
||||
</span>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/weekly-report/recipients/#{recipient}", method: :delete) do %>
|
||||
<svg
|
||||
class="w-4 h-4 text-red-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= form_for @conn, "/sites/#{URI.encode_www_form(@site.domain)}/weekly-report/recipients", fn f -> %>
|
||||
<div class="max-w-md mt-4">
|
||||
<div class="mt-1 flex rounded-md shadow-sm">
|
||||
<div class="relative flex items-stretch flex-grow focus-within:z-10">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
<.form
|
||||
for={nil}
|
||||
action={
|
||||
(meta.report && Routes.site_path(@conn, meta.disable_route, @site.domain)) ||
|
||||
Routes.site_path(@conn, meta.enable_route, @site.domain)
|
||||
}
|
||||
method="post"
|
||||
>
|
||||
<div>
|
||||
<.toggle_submit set_to={meta.report}>
|
||||
<%= meta.toggle %>
|
||||
</.toggle_submit>
|
||||
</div>
|
||||
</.form>
|
||||
|
||||
<div :if={meta.report} class="mt-4">
|
||||
<.table
|
||||
:if={Enum.count(meta.report.recipients) > 0}
|
||||
width="w-1/2"
|
||||
rows={meta.report.recipients}
|
||||
>
|
||||
<:thead>
|
||||
<.th>
|
||||
Recipients
|
||||
</.th>
|
||||
<.th invisible>Actions</.th>
|
||||
</:thead>
|
||||
|
||||
<:tbody :let={recipient}>
|
||||
<.td>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<Heroicons.envelope_open class="w-6 h-6 feather" />
|
||||
<div>
|
||||
<%= recipient %>
|
||||
</div>
|
||||
<%= email_input(f, :recipient,
|
||||
class:
|
||||
"focus:ring-indigo-500 dark:bg-gray-900 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500 dark:placeholder-gray-400 dark:text-gray-100",
|
||||
placeholder: "recipient@example.com",
|
||||
required: "true"
|
||||
) %>
|
||||
</div>
|
||||
</.td>
|
||||
<.td actions>
|
||||
<.delete_button
|
||||
method="delete"
|
||||
href={
|
||||
Routes.site_path(
|
||||
@conn,
|
||||
meta.remove_route,
|
||||
@site.domain,
|
||||
recipient
|
||||
)
|
||||
}
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
|
||||
<%= submit class: "-ml-px relative button rounded-l-none" do %>
|
||||
<svg
|
||||
class="w-5 h-5 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M8 9a3 3 0 100-6 3 3 0 000 6zM8 11a6 6 0 016 6H2a6 6 0 016-6zM16 7a1 1 0 10-2 0v1h-1a1 1 0 100 2h1v1a1 1 0 102 0v-1h1a1 1 0 100-2h-1V7z">
|
||||
</path>
|
||||
</svg>
|
||||
<span>Add recipient</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<.form
|
||||
:let={f}
|
||||
class="mt-4"
|
||||
for={@conn}
|
||||
action={Routes.site_path(@conn, meta.add_route, @site.domain)}
|
||||
method="post"
|
||||
>
|
||||
<div class="flex items-end gap-x-2">
|
||||
<PlausibleWeb.Live.Components.Form.input
|
||||
field={f[:recipient]}
|
||||
type="email"
|
||||
required
|
||||
placeholder="e.g. joe@example.com"
|
||||
mt?={false}
|
||||
/>
|
||||
<.button type="submit" mt?={false}>
|
||||
Add Recipient
|
||||
</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
</.form>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="my-8 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
<div class="my-8 flex items-center">
|
||||
<%= if @monthly_report do %>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/monthly-report/disable", method: :post, class: "bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %>
|
||||
<span class="translate-x-5 inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-800 shadow transform transition ease-in-out duration-200">
|
||||
</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/monthly-report/enable", method: :post, class: "bg-gray-200 dark:bg-gray-700 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %>
|
||||
<span class="translate-x-0 inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-800 shadow transform transition ease-in-out duration-200">
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<span class="ml-2 dark:text-gray-100">Send a monthly email report on 1st of the month</span>
|
||||
</div>
|
||||
<%= if @monthly_report do %>
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300 mt-6">
|
||||
<h4 class="font-bold my-2">Monthly report recipients</h4>
|
||||
<%= for recipient <- @monthly_report.recipients do %>
|
||||
<div class="p-2 pl-3 flex justify-between bg-gray-100 dark:bg-gray-900 rounded my-2 max-w-md">
|
||||
<span>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400 inline mr-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
<%= recipient %>
|
||||
</span>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/monthly-report/recipients/#{recipient}", method: :delete) do %>
|
||||
<svg
|
||||
class="w-4 h-4 text-red-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= form_for @conn, "/sites/#{URI.encode_www_form(@site.domain)}/monthly-report/recipients", fn f -> %>
|
||||
<div class="max-w-md mt-4">
|
||||
<div class="mt-1 flex rounded-md shadow-sm">
|
||||
<div class="relative flex items-stretch flex-grow focus-within:z-10">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
</.tile>
|
||||
|
||||
<% change_notifications = [
|
||||
spike: %{
|
||||
notification: @spike_notification,
|
||||
heading: "Traffic Spike Notifications",
|
||||
subtitle: "Get notified when your site has unusually high number of current visitors",
|
||||
threshold_text: "Current visitors threshold",
|
||||
toggle: "Send notifications of traffic spikes"
|
||||
},
|
||||
drop: %{
|
||||
notification: @drop_notification,
|
||||
heading: "Traffic Drop Notifications",
|
||||
subtitle:
|
||||
"Get notified when your site has unusually low number of visitors within 12 hours",
|
||||
threshold_text: "12 hours visitor threshold",
|
||||
toggle: "Send notifications of traffic drops"
|
||||
}
|
||||
] %>
|
||||
|
||||
<.tile :for={{type, meta} <- change_notifications} docs="traffic-spikes">
|
||||
<:title>
|
||||
<%= meta.heading %>
|
||||
</:title>
|
||||
<:subtitle>
|
||||
<%= meta.subtitle %>
|
||||
</:subtitle>
|
||||
|
||||
<.form
|
||||
for={nil}
|
||||
action={
|
||||
(meta.notification &&
|
||||
Routes.site_path(@conn, :disable_traffic_change_notification, @site.domain, type)) ||
|
||||
Routes.site_path(@conn, :enable_traffic_change_notification, @site.domain, type)
|
||||
}
|
||||
method="post"
|
||||
>
|
||||
<.toggle_submit set_to={meta.notification}>
|
||||
<%= meta.toggle %>
|
||||
</.toggle_submit>
|
||||
</.form>
|
||||
|
||||
<.form
|
||||
:let={f}
|
||||
:if={meta.notification}
|
||||
class="mt-4"
|
||||
for={Plausible.Site.TrafficChangeNotification.changeset(meta.notification, %{})}
|
||||
action={Routes.site_path(@conn, :update_traffic_change_notification, @site.domain, type)}
|
||||
>
|
||||
<div class="flex items-end gap-x-4">
|
||||
<PlausibleWeb.Live.Components.Form.input
|
||||
field={f[:threshold]}
|
||||
type="number"
|
||||
required
|
||||
label={meta.threshold_text}
|
||||
/>
|
||||
<.button type="submit" mt?={false}>
|
||||
Save Threshold
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
|
||||
<div class="mt-4">
|
||||
<.table
|
||||
:if={meta.notification && Enum.count(meta.notification.recipients) > 0}
|
||||
width="w-1/2"
|
||||
rows={meta.notification.recipients}
|
||||
>
|
||||
<:thead>
|
||||
<.th>
|
||||
Recipients
|
||||
</.th>
|
||||
<.th invisible>Actions</.th>
|
||||
</:thead>
|
||||
|
||||
<:tbody :let={recipient}>
|
||||
<.td>
|
||||
<div class="flex items-cetner gap-x-2">
|
||||
<Heroicons.envelope_open class="w-6 h-6 feather" />
|
||||
<div>
|
||||
<%= recipient %>
|
||||
</div>
|
||||
<%= email_input(f, :recipient,
|
||||
class:
|
||||
"focus:ring-indigo-500 dark:bg-gray-900 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500 dark:placeholder-gray-400 dark:text-gray-100",
|
||||
placeholder: "recipient@example.com",
|
||||
required: "true"
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<%= submit class: "-ml-px relative button rounded-l-none" do %>
|
||||
<svg
|
||||
class="w-5 h-5 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M8 9a3 3 0 100-6 3 3 0 000 6zM8 11a6 6 0 016 6H2a6 6 0 016-6zM16 7a1 1 0 10-2 0v1h-1a1 1 0 100 2h1v1a1 1 0 102 0v-1h1a1 1 0 100-2h-1V7z">
|
||||
</path>
|
||||
</svg>
|
||||
<span>Add recipient</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</.td>
|
||||
<.td actions>
|
||||
<.delete_button
|
||||
method="delete"
|
||||
href={
|
||||
Routes.site_path(
|
||||
@conn,
|
||||
:remove_traffic_change_notification_recipient,
|
||||
@site.domain,
|
||||
type,
|
||||
recipient
|
||||
)
|
||||
}
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= render("traffic_change_form",
|
||||
conn: @conn,
|
||||
notification: @spike_notification,
|
||||
site: @site,
|
||||
heading: "Traffic Spike Notifications",
|
||||
subtitle: "Get notified when your site has unusually high number of current visitors",
|
||||
toggle_text: "Send notifications of traffic spikes",
|
||||
threshold_label: "Current visitors threshold",
|
||||
type: :spike
|
||||
) %>
|
||||
|
||||
<%= render("traffic_change_form",
|
||||
conn: @conn,
|
||||
notification: @drop_notification,
|
||||
site: @site,
|
||||
heading: "Traffic Drop Notifications",
|
||||
subtitle: "Get notified when your site has unusually low number of visitors within 12 hours",
|
||||
toggle_text: "Send notifications of traffic drops",
|
||||
threshold_label: "12 hours visitor threshold",
|
||||
type: :drop
|
||||
) %>
|
||||
<div :if={meta.notification}>
|
||||
<.form
|
||||
:let={f}
|
||||
for={@conn}
|
||||
class="mt-4"
|
||||
action={
|
||||
Routes.site_path(
|
||||
@conn,
|
||||
:add_traffic_change_notification_recipient,
|
||||
@site.domain,
|
||||
type
|
||||
)
|
||||
}
|
||||
>
|
||||
<div class="flex items-end gap-x-4">
|
||||
<PlausibleWeb.Live.Components.Form.input
|
||||
field={f[:recipient]}
|
||||
type="email"
|
||||
placeholder="e.g. joe@example.com"
|
||||
mt?={false}
|
||||
required
|
||||
/>
|
||||
<.button type="submit" mt?={false}>
|
||||
Add Recipient
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
|
@ -1,28 +1,26 @@
|
||||
<section class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden">
|
||||
<PlausibleWeb.Components.Billing.Notice.premium_feature
|
||||
billable_user={@site.owner}
|
||||
current_user={@current_user}
|
||||
<.settings_tiles>
|
||||
<.tile
|
||||
docs="funnel-analysis"
|
||||
feature_mod={Plausible.Billing.Feature.Funnels}
|
||||
/>
|
||||
site={@site}
|
||||
conn={@conn}
|
||||
>
|
||||
<:title>
|
||||
Funnels
|
||||
</:title>
|
||||
<:subtitle>
|
||||
Compose Goals into Funnels
|
||||
</:subtitle>
|
||||
|
||||
<div class="py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Funnels</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Compose Goals into Funnels
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="funnel-analysis" />
|
||||
</header>
|
||||
|
||||
<PlausibleWeb.Components.Site.Feature.toggle
|
||||
feature_mod={Plausible.Billing.Feature.Funnels}
|
||||
site={@site}
|
||||
conn={@conn}
|
||||
>
|
||||
<div :if={Plausible.Billing.Feature.Funnels.enabled?(@site)}>
|
||||
<PlausibleWeb.Components.Billing.Notice.premium_feature
|
||||
billable_user={@site.owner}
|
||||
current_user={@current_user}
|
||||
feature_mod={Plausible.Billing.Feature.Funnels}
|
||||
/>
|
||||
<%= live_render(@conn, PlausibleWeb.Live.FunnelSettings,
|
||||
session: %{"site_id" => @site.id, "domain" => @site.domain}
|
||||
) %>
|
||||
</PlausibleWeb.Components.Site.Feature.toggle>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
|
@ -1,89 +1,52 @@
|
||||
<div class="shadow sm:rounded-md sm:overflow-hidden">
|
||||
<div class="bg-white dark:bg-gray-800 py-6 px-4 space-y-6 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Site Domain</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Moving your site to a different domain? We got you!
|
||||
</p>
|
||||
<.settings_tiles>
|
||||
<.tile docs="change-domain-name">
|
||||
<:title>
|
||||
Site Domain
|
||||
</:title>
|
||||
<:subtitle>
|
||||
Moving your site to a different domain? We got you!
|
||||
</:subtitle>
|
||||
<PlausibleWeb.Live.Components.Form.input
|
||||
name="domain"
|
||||
label="Domain"
|
||||
value={@site.domain}
|
||||
disabled
|
||||
width="w-1/2"
|
||||
/>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="change-domain-name" />
|
||||
</header>
|
||||
<div class="grid grid-cols-4 gap-6">
|
||||
<div class="col-span-4 sm:col-span-2">
|
||||
<%= label(nil, "Domain",
|
||||
class: "block text-sm font-medium leading-5 text-gray-700 dark:text-gray-300"
|
||||
) %>
|
||||
<%= text_input(nil, :domain,
|
||||
value: @site.domain,
|
||||
disabled: "disabled",
|
||||
class:
|
||||
"dark:bg-gray-900 w-full mt-1 block pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100 text-gray-500"
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<PlausibleWeb.Components.Generic.button_link href={
|
||||
Routes.site_path(@conn, :change_domain, @site.domain)
|
||||
}>
|
||||
Change Domain
|
||||
</PlausibleWeb.Components.Generic.button_link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<.button_link href={Routes.site_path(@conn, :change_domain, @site.domain)}>
|
||||
Change Domain
|
||||
</.button_link>
|
||||
</.tile>
|
||||
|
||||
<%= form_for @changeset, "/#{URI.encode_www_form(@site.domain)}/settings", fn f -> %>
|
||||
<div class="shadow sm:rounded-md sm:overflow-hidden">
|
||||
<div class="bg-white dark:bg-gray-800 py-6 px-4 space-y-6 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
Site Timezone
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Update your reporting timezone.
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="general" />
|
||||
</header>
|
||||
<div class="grid grid-cols-4 gap-6">
|
||||
<div class="col-span-4 sm:col-span-2">
|
||||
<%= label(f, :timezone, "Reporting Timezone",
|
||||
class: "block text-sm font-medium leading-5 text-gray-700 dark:text-gray-300"
|
||||
) %>
|
||||
<%= select(f, :timezone, Plausible.Timezones.options(),
|
||||
class:
|
||||
"dark:bg-gray-900 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100 cursor-pointer"
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
||||
<PlausibleWeb.Components.Generic.button type="submit">
|
||||
Save
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="shadow sm:rounded-md sm:overflow-hidden">
|
||||
<div class="bg-white dark:bg-gray-800 py-6 px-4 space-y-6 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
<a id="snippet">Site Installation</a>
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Control what data is collected and verify your installation.
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="plausible-script" />
|
||||
</header>
|
||||
|
||||
<div class="my-4">
|
||||
<PlausibleWeb.Components.Generic.button_link
|
||||
class="mt-4"
|
||||
href={
|
||||
Routes.site_path(@conn, :installation, @site.domain, flow: PlausibleWeb.Flows.review())
|
||||
}
|
||||
>
|
||||
Review Installation
|
||||
</PlausibleWeb.Components.Generic.button_link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<.tile docs="general">
|
||||
<:title>Site Timezone</:title>
|
||||
<:subtitle>Update your reporting timezone</:subtitle>
|
||||
<%= form_for @changeset, "/#{URI.encode_www_form(@site.domain)}/settings", fn f -> %>
|
||||
<PlausibleWeb.Live.Components.Form.input
|
||||
field={f[:timezone]}
|
||||
label="Reporting Timezone"
|
||||
type="select"
|
||||
options={Plausible.Timezones.options()}
|
||||
width="w-1/2"
|
||||
/>
|
||||
<.button type="submit">
|
||||
Save timezone
|
||||
</.button>
|
||||
<% end %>
|
||||
</.tile>
|
||||
<.tile docs="plausible-script">
|
||||
<:title>Site Installation</:title>
|
||||
<:subtitle>
|
||||
Control what data is collected and verify your installation.
|
||||
</:subtitle>
|
||||
<.button_link
|
||||
class="mt-4"
|
||||
href={
|
||||
Routes.site_path(@conn, :installation, @site.domain, flow: PlausibleWeb.Flows.review())
|
||||
}
|
||||
>
|
||||
Review Installation
|
||||
</.button_link>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
|
@ -1,26 +1,29 @@
|
||||
<div class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Goals</h2>
|
||||
<p class="mt-2 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Define actions that you want your users to take, like visiting a certain page, submitting a form, etc.
|
||||
</p>
|
||||
<p :if={ee?()} class="text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
You can also <a
|
||||
href={Routes.site_path(@conn, :settings_funnels, @site.domain)}
|
||||
class="text-indigo-500 underline"
|
||||
>compose Goals into Funnels</a>.
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="goal-conversions" />
|
||||
</header>
|
||||
|
||||
<PlausibleWeb.Components.Site.Feature.toggle
|
||||
<.settings_tiles>
|
||||
<.tile
|
||||
docs="goal-conversions"
|
||||
feature_mod={Plausible.Billing.Feature.Goals}
|
||||
site={@site}
|
||||
conn={@conn}
|
||||
>
|
||||
<%= live_render(@conn, PlausibleWeb.Live.GoalSettings,
|
||||
session: %{"site_id" => @site.id, "domain" => @site.domain}
|
||||
) %>
|
||||
</PlausibleWeb.Components.Site.Feature.toggle>
|
||||
</div>
|
||||
<:title>
|
||||
Goals
|
||||
</:title>
|
||||
<:subtitle>
|
||||
<p>
|
||||
Define actions that you want your users to take, like visiting a certain page, submitting a form, etc.
|
||||
</p>
|
||||
<p :if={ee?()}>
|
||||
You can also
|
||||
<.styled_link href={Routes.site_path(@conn, :settings_funnels, @site.domain)}>
|
||||
compose Goals into Funnels
|
||||
</.styled_link>
|
||||
</p>
|
||||
</:subtitle>
|
||||
|
||||
<div :if={Plausible.Billing.Feature.Goals.enabled?(@site)}>
|
||||
<%= live_render(@conn, PlausibleWeb.Live.GoalSettings,
|
||||
session: %{"site_id" => @site.id, "domain" => @site.domain}
|
||||
) %>
|
||||
</div>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
|
@ -1,36 +1,33 @@
|
||||
<div class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6">
|
||||
<header class="relative border-b border-gray-200 pb-4">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
<.settings_tiles docs="google-analytics-import">
|
||||
<.tile>
|
||||
<:title>
|
||||
Import Data
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
</:title>
|
||||
<:subtitle>
|
||||
Import existing data from external sources.
|
||||
Pick one of the options below to start a new import.
|
||||
</p>
|
||||
Pick one of the options below to start a new import. <br />
|
||||
A maximum of <%= Plausible.Imported.max_complete_imports() %> imports at any time is allowed.
|
||||
</:subtitle>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="google-analytics-import" />
|
||||
</header>
|
||||
<%= live_render(@conn, PlausibleWeb.Live.ImportsExportsSettings,
|
||||
session: %{"domain" => @site.domain}
|
||||
) %>
|
||||
</.tile>
|
||||
|
||||
<%= live_render(@conn, PlausibleWeb.Live.ImportsExportsSettings,
|
||||
session: %{"domain" => @site.domain}
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<div class="shadow bg-white dark:bg-gray-800 dark:text-gray-200 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6">
|
||||
<header class="relative border-b border-gray-200 pb-4 mb-5">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
<.tile>
|
||||
<:title>
|
||||
Export Data
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Export all your data into CSV format.
|
||||
</p>
|
||||
</header>
|
||||
</:title>
|
||||
<:subtitle>
|
||||
Export all your data into CSV format
|
||||
</:subtitle>
|
||||
|
||||
<%= live_render(@conn, PlausibleWeb.Live.CSVExport,
|
||||
session: %{
|
||||
"site_id" => @site.id,
|
||||
"email_to" => @current_user.email,
|
||||
"storage" => on_ee(do: "s3", else: "local")
|
||||
}
|
||||
) %>
|
||||
</div>
|
||||
<%= live_render(@conn, PlausibleWeb.Live.CSVExport,
|
||||
session: %{
|
||||
"site_id" => @site.id,
|
||||
"email_to" => @current_user.email,
|
||||
"storage" => on_ee(do: "s3", else: "local")
|
||||
}
|
||||
) %>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
|
@ -1,21 +1,17 @@
|
||||
<PlausibleWeb.Components.Settings.settings_search_console
|
||||
site={@site}
|
||||
search_console_domains={@search_console_domains}
|
||||
/>
|
||||
<.settings_tiles>
|
||||
<PlausibleWeb.Components.Settings.settings_search_console
|
||||
conn={@conn}
|
||||
site={@site}
|
||||
search_console_domains={@search_console_domains}
|
||||
/>
|
||||
|
||||
<section
|
||||
:if={@has_plugins_tokens? || @conn.query_params["new_token"]}
|
||||
class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden"
|
||||
>
|
||||
<div class="py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
Plugin Tokens
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Control Plugin Access
|
||||
</p>
|
||||
</header>
|
||||
<.tile :if={@has_plugins_tokens? || @conn.query_params["new_token"]}>
|
||||
<:title>
|
||||
Plugin Tokens
|
||||
</:title>
|
||||
<:subtitle>
|
||||
Control Plugin Access
|
||||
</:subtitle>
|
||||
|
||||
<%= live_render(@conn, PlausibleWeb.Live.Plugins.API.Settings,
|
||||
session: %{
|
||||
@ -24,5 +20,5 @@
|
||||
"new_token" => @conn.query_params["new_token"]
|
||||
}
|
||||
) %>
|
||||
</div>
|
||||
</section>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
|
@ -1,242 +1,218 @@
|
||||
<div class="shadow bg-white dark:bg-gray-800 sm:rounded-md py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">People</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Invite your friends or coworkers
|
||||
</p>
|
||||
<.settings_tiles>
|
||||
<.tile docs="user-roles">
|
||||
<:title>People</:title>
|
||||
<:subtitle>Invite your friends or coworkers</:subtitle>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="users-roles" />
|
||||
</header>
|
||||
<div class="flow-root mt-6">
|
||||
<ul class="-my-5 divide-y divide-gray-200 dark:divide-gray-400">
|
||||
<%= for membership <- @site.memberships do %>
|
||||
<li class="py-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<%= img_tag(Plausible.Auth.User.profile_img_url(membership.user),
|
||||
class: "h-8 w-8 rounded-full"
|
||||
) %>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-50 truncate">
|
||||
<%= membership.user.name %>
|
||||
<PlausibleWeb.Components.Generic.styled_link
|
||||
:if={ee?() and Plausible.Auth.is_super_admin?(@current_user)}
|
||||
new_tab={true}
|
||||
href={PlausibleWeb.Endpoint.url() <> "/crm/auth/user/#{membership.user.id}"}
|
||||
>
|
||||
CRM
|
||||
</PlausibleWeb.Components.Generic.styled_link>
|
||||
</p>
|
||||
<p class="text-sm text-gray-400 truncate">
|
||||
<%= membership.user.email %>
|
||||
</p>
|
||||
</div>
|
||||
<.filter_bar filtering_enabled?={false}>
|
||||
<.button_link
|
||||
mt?={false}
|
||||
href={Routes.membership_path(@conn, :invite_member_form, @site.domain)}
|
||||
>
|
||||
Invite new member
|
||||
</.button_link>
|
||||
</.filter_bar>
|
||||
|
||||
<div x-data="{open: false}" @click.away="open = false" x-cloak class="relative">
|
||||
<button
|
||||
@click="open = !open"
|
||||
class="inline-flex items-center shadow-sm px-2.5 py-0.5 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-full bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
<%= membership.role |> Atom.to_string() |> String.capitalize() %>
|
||||
<svg
|
||||
class="w-4 h-4 pt-px ml-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
<div class="flow-root">
|
||||
<ul class="divide-y divide-gray-200 dark:divide-gray-400">
|
||||
<%= for membership <- @site.memberships do %>
|
||||
<li class="py-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<%= img_tag(Plausible.Auth.User.profile_img_url(membership.user),
|
||||
class: "h-8 w-8 rounded-full"
|
||||
) %>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm truncate">
|
||||
<span class="font-medium text-gray-900 dark:text-gray-50">
|
||||
<%= membership.user.name %>
|
||||
</span>
|
||||
<br />
|
||||
<span class="text-gray-500 dark:text-gray-400">
|
||||
<%= membership.user.email %>
|
||||
</span>
|
||||
<.styled_link
|
||||
:if={ee?() and Plausible.Auth.is_super_admin?(@current_user)}
|
||||
new_tab={true}
|
||||
href={PlausibleWeb.Endpoint.url() <> "/crm/auth/user/#{membership.user.id}"}
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
<ul
|
||||
x-show="open"
|
||||
x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="origin-top-right absolute z-10 right-0 mt-2 w-72 rounded-md shadow-lg overflow-hidden bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-400 ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
tabindex="-1"
|
||||
role="listbox"
|
||||
aria-labelledby="listbox-label"
|
||||
aria-activedescendant="listbox-option-0"
|
||||
>
|
||||
<%= if membership.role == :owner do %>
|
||||
<li class="p-4 text-sm cursor-default group flex justify-between" role="option">
|
||||
<div>
|
||||
<p class="text-base font-medium text-gray-900 dark:text-gray-100">Owner</p>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Site owner cannot be assigned to any other role
|
||||
</p>
|
||||
</div>
|
||||
CRM
|
||||
</.styled_link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span class="text-indigo-500">
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<%= if @conn.assigns[:current_user_role] == :owner do %>
|
||||
<li
|
||||
class="select-none hover:bg-gray-100 dark:hover:bg-gray-900 text-red-600"
|
||||
role="option"
|
||||
<div x-data="{open: false}" @click.away="open = false" x-cloak class="relative">
|
||||
<button
|
||||
@click="open = !open"
|
||||
class="inline-flex items-center shadow-sm px-2.5 py-0.5 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-full bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
<%= membership.role |> Atom.to_string() |> String.capitalize() %>
|
||||
<svg
|
||||
class="w-4 h-4 pt-px ml-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
>
|
||||
<%= link("Transfer ownership →",
|
||||
to: Routes.membership_path(@conn, :transfer_ownership_form, @site.domain),
|
||||
class: "inline-block w-full p-4 text-sm text-red-600 font-medium"
|
||||
) %>
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
<ul
|
||||
x-show="open"
|
||||
x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="origin-top-right absolute z-50 right-0 mt-2 w-72 rounded-md shadow-lg overflow-hidden bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-400 ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
tabindex="-1"
|
||||
role="listbox"
|
||||
aria-labelledby="listbox-label"
|
||||
aria-activedescendant="listbox-option-0"
|
||||
>
|
||||
<%= if membership.role == :owner do %>
|
||||
<li class="p-4 cursor-default group flex justify-between" role="option">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Owner
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Site owner cannot be assigned to any other role
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span class="text-indigo-500">
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link(to: Routes.membership_path(@conn, :update_role, @site.domain, membership.id, "admin"), method: :put, class: "p-4 flex justify-between text-sm group hover:bg-indigo-500") do %>
|
||||
<div>
|
||||
<p class="text-base font-medium text-gray-900 dark:text-gray-100 group-hover:text-white">
|
||||
Admin
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-300 group-hover:text-gray-100 dark:group-hover:text-white">
|
||||
View stats and edit site settings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= if membership.role == :admin do %>
|
||||
<span class="text-indigo-500 group-hover:text-white">
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= link(to: Routes.membership_path(@conn, :update_role, @site.domain, membership.id, "viewer"), method: :put, class: "p-4 flex justify-between text-sm group hover:bg-indigo-500") do %>
|
||||
<div>
|
||||
<p class="text-base font-medium text-gray-900 dark:text-gray-100 group-hover:text-white">
|
||||
Viewer
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-300 group-hover:text-gray-100 dark:group-hover:text-white">
|
||||
View stats only
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= if membership.role == :viewer do %>
|
||||
<span class="text-indigo-500 group-hover:text-white">
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= link(to: Routes.membership_path(@conn, :remove_member, @site.domain, membership.id), method: :delete, class: "p-4 flex hover:bg-gray-100 hover:bg-gray-900 text-red-600") do %>
|
||||
<p class="text-sm text-red-600 font-medium">Remove member</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<%= if Enum.count(@site.invitations) > 0 do %>
|
||||
<header class="mt-12">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
Pending invitations
|
||||
</h2>
|
||||
</header>
|
||||
<div class="flex flex-col mt-4">
|
||||
<div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||
<div class="shadow overflow-hidden border-b border-gray-200 dark:border-gray-500 sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-400">
|
||||
<thead class="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-200 uppercase tracking-wider"
|
||||
>
|
||||
Email
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-200 uppercase tracking-wider"
|
||||
>
|
||||
Role
|
||||
</th>
|
||||
<th scope="col" class="relative px-6 py-3">
|
||||
<span class="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for invitation <- @site.invitations do %>
|
||||
<tr class="odd:bg-white even:bg-gray-50 dark:odd:bg-gray-850 dark:even:bg-gray-825">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<%= invitation.email %>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-200">
|
||||
<%= invitation.role |> Atom.to_string() |> String.capitalize() %>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<%= link("Remove",
|
||||
<%= if @conn.assigns[:current_user_role] == :owner do %>
|
||||
<li
|
||||
class="select-none hover:bg-gray-100 dark:hover:bg-gray-900 text-red-600"
|
||||
role="option"
|
||||
>
|
||||
<%= link("Transfer ownership →",
|
||||
to:
|
||||
Routes.invitation_path(
|
||||
Routes.membership_path(
|
||||
@conn,
|
||||
:remove_invitation,
|
||||
@site.domain,
|
||||
invitation.invitation_id
|
||||
:transfer_ownership_form,
|
||||
@site.domain
|
||||
),
|
||||
method: :delete,
|
||||
class: "text-red-600 hover:text-red-900"
|
||||
class: "inline-block w-full text-sm p-4 text-red-600 font-medium"
|
||||
) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link(to: Routes.membership_path(@conn, :update_role, @site.domain, membership.id, "admin"), method: :put, class: "p-4 flex justify-between group hover:bg-indigo-500") do %>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 group-hover:text-white">
|
||||
Admin
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400 group-hover:text-gray-100 dark:group-hover:text-white">
|
||||
View stats and edit site settings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<PlausibleWeb.Components.Generic.button_link href={
|
||||
Routes.membership_path(@conn, :invite_member_form, @site.domain)
|
||||
}>
|
||||
<Heroicons.user_plus solid class="w-5 h-5" /> Invite
|
||||
</PlausibleWeb.Components.Generic.button_link>
|
||||
</div>
|
||||
</div>
|
||||
<%= if membership.role == :admin do %>
|
||||
<span class="text-indigo-500 group-hover:text-white">
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= link(to: Routes.membership_path(@conn, :update_role, @site.domain, membership.id, "viewer"), method: :put, class: "p-4 flex justify-between group hover:bg-indigo-500") do %>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 group-hover:text-white">
|
||||
Viewer
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400 group-hover:text-gray-100 dark:group-hover:text-white">
|
||||
View stats only
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= if membership.role == :viewer do %>
|
||||
<span class="text-indigo-500 group-hover:text-white">
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= link(to: Routes.membership_path(@conn, :remove_member, @site.domain, membership.id), method: :delete, class: "p-4 flex hover:bg-gray-100 dark:hover:bg-gray-900 text-red-600") do %>
|
||||
<p class="text-red-600 font-medium text-sm">Remove member</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</.tile>
|
||||
|
||||
<.tile :if={Enum.count(@site.invitations) > 0}>
|
||||
<:title>Pending invitations</:title>
|
||||
<:subtitle>Waiting for new members to accept their invitations</:subtitle>
|
||||
|
||||
<.table rows={@site.invitations}>
|
||||
<:thead>
|
||||
<.th>Email</.th>
|
||||
<.th hide_on_mobile>Role</.th>
|
||||
<.th invisible>Actions</.th>
|
||||
</:thead>
|
||||
<:tbody :let={invitation}>
|
||||
<.td><%= invitation.email %></.td>
|
||||
<.td hide_on_mobile><%= Phoenix.Naming.humanize(invitation.role) %></.td>
|
||||
<.td actions>
|
||||
<.delete_button
|
||||
href={
|
||||
Routes.invitation_path(
|
||||
@conn,
|
||||
:remove_invitation,
|
||||
@site.domain,
|
||||
invitation.invitation_id
|
||||
)
|
||||
}
|
||||
method="delete"
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
|
@ -1,40 +1,29 @@
|
||||
<section class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden">
|
||||
<PlausibleWeb.Components.Billing.Notice.premium_feature
|
||||
billable_user={@site.owner}
|
||||
current_user={@current_user}
|
||||
<.settings_tiles>
|
||||
<.tile
|
||||
docs="custom-props/introduction"
|
||||
feature_mod={Plausible.Billing.Feature.Props}
|
||||
grandfathered?
|
||||
/>
|
||||
site={@site}
|
||||
conn={@conn}
|
||||
>
|
||||
<:title>
|
||||
Custom Properties
|
||||
</:title>
|
||||
<:subtitle>
|
||||
Attach Custom Properties when sending a Pageview or an Event to
|
||||
create custom metrics.
|
||||
</:subtitle>
|
||||
|
||||
<div class="py-6 px-4 sm:p-6">
|
||||
<header class="w-full flex relative">
|
||||
<span class="flex-1">
|
||||
<h1 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
Custom Properties
|
||||
</h1>
|
||||
|
||||
<p class="mt-2 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Attach Custom Properties when sending a Pageview or an Event to
|
||||
create custom metrics.
|
||||
</p>
|
||||
<p class="text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
In order for the properties to show up on your dashboard, you need to
|
||||
explicitly add them below first.
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="custom-props/introduction" />
|
||||
</header>
|
||||
|
||||
<PlausibleWeb.Components.Site.Feature.toggle
|
||||
feature_mod={Plausible.Billing.Feature.Props}
|
||||
site={@site}
|
||||
conn={@conn}
|
||||
>
|
||||
<div :if={Plausible.Billing.Feature.Props.enabled?(@site)}>
|
||||
<PlausibleWeb.Components.Billing.Notice.premium_feature
|
||||
billable_user={@site.owner}
|
||||
current_user={@current_user}
|
||||
feature_mod={Plausible.Billing.Feature.Props}
|
||||
grandfathered?
|
||||
/>
|
||||
<%= live_render(@conn, PlausibleWeb.Live.PropsSettings,
|
||||
id: "props-form",
|
||||
session: %{"site_id" => @site.id, "domain" => @site.domain}
|
||||
) %>
|
||||
</PlausibleWeb.Components.Site.Feature.toggle>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
|
@ -1,38 +1,38 @@
|
||||
<div class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6">
|
||||
<header class="relative border-b border-gray-200 pb-4">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
Google Search Console Integration
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
<.tile docs="google-search-console-integration">
|
||||
<:title>
|
||||
Google Search Console Integration
|
||||
</:title>
|
||||
<:subtitle>
|
||||
<p>
|
||||
You can integrate with Google Search Console to get all of your important search results stats such as keyword phrases people find your site with.
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="google-search-console-integration" />
|
||||
</header>
|
||||
|
||||
</:subtitle>
|
||||
<%= if Keyword.get(Application.get_env(:plausible, :google), :client_id) do %>
|
||||
<%= if @site.google_auth do %>
|
||||
<div class="flex py-8">
|
||||
<span class="flex-1 text-gray-700 dark:text-gray-300">
|
||||
Linked Google account: <b><%= @site.google_auth.email %></b>
|
||||
</span>
|
||||
<PlausibleWeb.Live.Components.Form.input
|
||||
name="account"
|
||||
label="Linked Google account"
|
||||
value={@site.google_auth.email}
|
||||
disabled="disabled"
|
||||
width="w-1/2"
|
||||
/>
|
||||
|
||||
<%= link("Unlink Google account",
|
||||
to: "/#{URI.encode_www_form(@site.domain)}/settings/google-search",
|
||||
class:
|
||||
"inline-block px-4 text-sm leading-5 font-medium text-red-600 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150",
|
||||
method: "delete"
|
||||
) %>
|
||||
</div>
|
||||
<.button_link
|
||||
theme="danger"
|
||||
href={Routes.site_path(@conn, :delete_google_auth, @site.domain)}
|
||||
method="delete"
|
||||
>
|
||||
Unlink Google Account
|
||||
</.button_link>
|
||||
|
||||
<%= case @search_console_domains do %>
|
||||
<% {:ok, domains} -> %>
|
||||
<%= if @site.google_auth.property && !(@site.google_auth.property in domains) do %>
|
||||
<p class="text-gray-700 dark:text-gray-300 mt-6 font-bold">
|
||||
NB: Your Google account does not have access to your currently configured property, <%= @site.google_auth.property %>. Please select a verified property from the list below.
|
||||
</p>
|
||||
<.notice class="mt-4 mb-4">
|
||||
Your Google account does not have access to your currently configured property, <%= @site.google_auth.property %>. Please select a verified property from the list below.
|
||||
</.notice>
|
||||
<% else %>
|
||||
<p class="text-gray-700 dark:text-gray-300 mt-6">
|
||||
<p class="text-sm mt-4">
|
||||
Select the Google Search Console property you would like to pull keyword data from. If you don't see your domain,
|
||||
<.styled_link
|
||||
href="https://plausible.io/docs/google-search-console-integration"
|
||||
@ -45,46 +45,40 @@
|
||||
<% end %>
|
||||
|
||||
<%= form_for Plausible.Site.GoogleAuth.changeset(@site.google_auth), "/#{URI.encode_www_form(@site.domain)}/settings/google", [class: "max-w-xs"], fn f -> %>
|
||||
<div class="my-6">
|
||||
<div class="inline-block relative w-full">
|
||||
<%= select(f, :property, domains,
|
||||
prompt: "(Choose property)",
|
||||
class:
|
||||
"dark:bg-gray-800 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 outline-none focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100"
|
||||
) %>
|
||||
</div>
|
||||
<div class="inline-block relative w-full">
|
||||
<%= select(f, :property, domains,
|
||||
prompt: "(Choose property)",
|
||||
class:
|
||||
"dark:bg-gray-800 mt-1 block w-full pl-3 pr-10 py-2 border-gray-300 dark:border-gray-500 outline-none focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:rounded-md "
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<%= submit("Save", class: "button") %>
|
||||
<.button type="submit">Save</.button>
|
||||
<% end %>
|
||||
<% {:error, error} -> %>
|
||||
<p class="text-gray-700 dark:text-gray-300 mt-6">
|
||||
The following error happened when fetching your Google Search Console domains:
|
||||
</p>
|
||||
|
||||
<%= case error do %>
|
||||
<% "invalid_grant" -> %>
|
||||
<p class="text-red-700 font-medium mt-3">
|
||||
<a href="https://plausible.io/docs/google-search-console-integration#i-get-the-invalid-grant-error">
|
||||
Invalid Grant error returned from Google. <span class="text-indigo-500">See here on how to fix it</span>.
|
||||
</a>
|
||||
</p>
|
||||
<% "google_auth_error" -> %>
|
||||
<p class="text-red-700 font-medium mt-3">
|
||||
<.notice title="Integration Error" theme={:red} class="mt-8">
|
||||
<%= case error do %>
|
||||
<% "invalid_grant" -> %>
|
||||
Invalid Grant error returned from Google.
|
||||
<.styled_link
|
||||
new_tab={true}
|
||||
href="https://plausible.io/docs/google-search-console-integration#i-get-the-invalid-grant-error"
|
||||
>
|
||||
See here on how to fix it
|
||||
</.styled_link>
|
||||
<% "google_auth_error" -> %>
|
||||
Your Search Console account hasn't been connected successfully. Please unlink your Google account and try linking it again.
|
||||
</p>
|
||||
<% _ -> %>
|
||||
<p class="text-red-700 font-medium mt-3">
|
||||
<% _ -> %>
|
||||
Something went wrong, but looks temporary. If the problem persists, try re-linking your Google account.
|
||||
</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</.notice>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<PlausibleWeb.Components.Google.button
|
||||
id="search-console-connect"
|
||||
to={Plausible.Google.API.search_console_authorize_url(@site.id)}
|
||||
/>
|
||||
<div class="text-gray-700 dark:text-gray-300 mt-8">
|
||||
<div class="mt-8 text-sm">
|
||||
NB: You also need to set up your site on
|
||||
<.styled_link href="https://search.google.com/search-console/about" new_tab={true}>
|
||||
Google Search Console
|
||||
@ -115,7 +109,7 @@
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
<p class="text-gray-900 dark:text-gray-200">
|
||||
<p>
|
||||
An extra step is needed to set up your <%= Plausible.product_name() %> for the Google Search Console integration.
|
||||
Find instructions <%= link("here",
|
||||
to: "https://github.com/plausible/community-edition/wiki/google-integration",
|
||||
@ -124,4 +118,4 @@
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</.tile>
|
||||
|
@ -1,270 +1,147 @@
|
||||
<div class="px-4 py-6 bg-white shadow dark:bg-gray-800 sm:rounded-md sm:overflow-hidden sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg font-medium text-gray-900 leading-6 dark:text-gray-100">
|
||||
Public dashboard
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 leading-5 dark:text-gray-200">
|
||||
<.settings_tiles>
|
||||
<.tile docs="visibility">
|
||||
<:title>
|
||||
Public Dashboard
|
||||
</:title>
|
||||
<:subtitle>
|
||||
Share your stats publicly or keep them private
|
||||
</p>
|
||||
</:subtitle>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="visibility" />
|
||||
</header>
|
||||
|
||||
<%= if @site.public do %>
|
||||
<div class="flex items-center mt-4 space-x-3">
|
||||
<%= button(to: Routes.site_path(@conn, :make_private, @site.domain), method: "POST", class: "bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %>
|
||||
<span class="inline-block w-5 h-5 bg-white rounded-full shadow translate-x-5 dark:bg-gray-800 transform transition ease-in-out duration-200">
|
||||
</span>
|
||||
<% end %>
|
||||
<span class="text-sm font-medium text-gray-900 leading-5 dark:text-gray-100">
|
||||
Stats are publicly available on <%= link(PlausibleWeb.StatsView.pretty_stats_url(@site),
|
||||
to: Routes.stats_path(@conn, :stats, @site.domain, []),
|
||||
class: "text-indigo-500"
|
||||
) %>
|
||||
</span>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="flex items-center mt-4 space-x-3">
|
||||
<%= button(to: Routes.site_path(@conn, :make_public, @site.domain), method: "POST", class: "bg-gray-200 dark:bg-gray-700 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %>
|
||||
<span class="inline-block w-5 h-5 bg-white rounded-full shadow translate-x-0 dark:bg-gray-800 transform transition ease-in-out duration-200">
|
||||
</span>
|
||||
<% end %>
|
||||
<span class="text-sm font-medium text-gray-900 leading-5 dark:text-gray-100">
|
||||
<.form
|
||||
action={
|
||||
(@site.public && Routes.site_path(@conn, :make_private, @site.domain)) ||
|
||||
Routes.site_path(@conn, :make_public, @site.domain)
|
||||
}
|
||||
method="post"
|
||||
for={nil}
|
||||
>
|
||||
<.toggle_submit set_to={@site.public}>
|
||||
Make stats publicly available on <%= link(PlausibleWeb.StatsView.pretty_stats_url(@site),
|
||||
to: Routes.stats_path(@conn, :stats, @site.domain, []),
|
||||
class: "text-indigo-500"
|
||||
) %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</.toggle_submit>
|
||||
</.form>
|
||||
</.tile>
|
||||
|
||||
<div class="px-4 py-6 bg-white shadow dark:bg-gray-800 sm:rounded-md sm:overflow-hidden sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg font-medium text-gray-900 leading-6 dark:text-gray-100">Shared Links</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 leading-5 dark:text-gray-200">
|
||||
<.tile docs="shared-links">
|
||||
<:title>
|
||||
Shared Links
|
||||
</:title>
|
||||
<:subtitle>
|
||||
You can share your stats privately by generating a shared link. The links are impossible to guess and you can add password protection for extra security.
|
||||
</:subtitle>
|
||||
|
||||
<.filter_bar filtering_enabled?={false}>
|
||||
<.button_link href={Routes.site_path(@conn, :new_shared_link, @site.domain)} mt?={false}>
|
||||
Add Shared Link
|
||||
</.button_link>
|
||||
</.filter_bar>
|
||||
|
||||
<p :if={Enum.empty?(@shared_links)} class="mb-8 text-center text-sm">
|
||||
No Shared Links configured for this site.
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="shared-links" />
|
||||
</header>
|
||||
|
||||
<div class="mt-6 flex flex-col divide-y divide-gray-200">
|
||||
<%= for link <- @shared_links do %>
|
||||
<div class="py-4">
|
||||
<label
|
||||
for={link.slug}
|
||||
class="flex content-center text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<.table rows={@shared_links}>
|
||||
<:thead>
|
||||
<.th hide_on_mobile>Name</.th>
|
||||
<.th>Link</.th>
|
||||
<.th invisible>Actions</.th>
|
||||
</:thead>
|
||||
<:tbody :let={link}>
|
||||
<.td truncate hide_on_mobile>
|
||||
<%= link.name %>
|
||||
<%= if link.password_hash do %>
|
||||
<svg
|
||||
class="ml-1 w-4 h-4 mt-px"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
|
||||
clip-rule="evenodd"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
<% else %>
|
||||
<svg
|
||||
class="ml-1 w-4 h-4 mt-px"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M10 2a5 5 0 00-5 5v2a2 2 0 00-2 2v5a2 2 0 002 2h10a2 2 0 002-2v-5a2 2 0 00-2-2H7V7a3 3 0 015.905-.75 1 1 0 001.937-.5A5.002 5.002 0 0010 2z">
|
||||
</path>
|
||||
</svg>
|
||||
<% end %>
|
||||
</label>
|
||||
<div class="relative flex w-full mt-2 text-sm">
|
||||
<input
|
||||
type="text"
|
||||
<Heroicons.lock_closed :if={link.password_hash} class="w-6 h-6 feather ml-2" />
|
||||
<Heroicons.lock_open :if={!link.password_hash} class="w-6 h-6 feather ml-2" />
|
||||
</.td>
|
||||
<.td>
|
||||
<PlausibleWeb.Live.Components.Form.input_with_clipboard
|
||||
name={link.slug}
|
||||
id={link.slug}
|
||||
readonly="readonly"
|
||||
value={shared_link_dest(@site, link)}
|
||||
class="w-full p-2 text-gray-700 bg-gray-100 border-none rounded rounded-r-none outline-none appearance-none transition dark:bg-gray-900 dark:text-gray-300 focus:outline-none focus:border-gray-300 dark:focus:border-gray-500"
|
||||
/>
|
||||
<button
|
||||
onclick={"var input = document.getElementById('#{link.slug}'); input.focus(); input.select(); document.execCommand('copy');"}
|
||||
href="javascript:void(0)"
|
||||
class="px-4 py-2 inline-flex items-center text-indigo-800 bg-gray-200 border-r border-gray-300 rounded-none dark:bg-gray-850 dark:text-indigo-500 dark:border-gray-500 hover:bg-gray-300 dark:hover:bg-gray-825"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z"></path>
|
||||
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z">
|
||||
</path>
|
||||
</svg>
|
||||
<span class="ml-1">Copy</span>
|
||||
</button>
|
||||
</.td>
|
||||
<.td actions>
|
||||
<.edit_button
|
||||
class="mt-2"
|
||||
href={Routes.site_path(@conn, :edit_shared_link, @site.domain, link.slug)}
|
||||
/>
|
||||
<.delete_button
|
||||
class="mt-2"
|
||||
method="delete"
|
||||
href={Routes.site_path(@conn, :delete_shared_link, @site.domain, link.slug)}
|
||||
data-confirm="Are you sure you want to delete this shared link? The stats will not be accessible with this link anymore."
|
||||
/>
|
||||
</.td>
|
||||
</:tbody>
|
||||
</.table>
|
||||
</.tile>
|
||||
|
||||
<%= link(to: Routes.site_path(@conn, :edit_shared_link, @site.domain, link.slug), class: "px-4 py-2 inline-flex items-center text-indigo-800 bg-gray-200 border-r border-gray-300 rounded-none dark:bg-gray-850 dark:text-indigo-500 dark:border-gray-500 hover:bg-gray-300 dark:hover:bg-gray-825") do %>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z">
|
||||
</path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
|
||||
clip-rule="evenodd"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
<% end %>
|
||||
<%= button(to: Routes.site_path(@conn, :delete_shared_link, @site.domain, link.slug), method: :delete, class: "py-2 px-4 inline-flex items-center bg-gray-200 dark:bg-gray-850 text-red-600 dark:text-red-500 rounded-l-none hover:bg-gray-300 dark:hover:bg-gray-825", data: [confirm: "Are you sure you want to delete this shared link? The stats will not be accessible with this link anymore."]) do %>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<PlausibleWeb.Components.Generic.button_link
|
||||
href={Routes.site_path(@conn, :new_shared_link, @site.domain)}
|
||||
class="mt-4"
|
||||
>
|
||||
+ New Link
|
||||
</PlausibleWeb.Components.Generic.button_link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-6 bg-white shadow dark:bg-gray-800 sm:rounded-md sm:overflow-hidden sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg font-medium text-gray-900 leading-6 dark:text-gray-100">
|
||||
<.tile docs="embed-dashboard">
|
||||
<:title>
|
||||
Embed Dashboard
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 leading-5 dark:text-gray-200">
|
||||
</:title>
|
||||
<:subtitle>
|
||||
You can use shared links to embed your stats in any other webpage using an <code>iframe</code>. Copy & paste a shared link into the form below to generate the embed code.
|
||||
</p>
|
||||
</:subtitle>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="embed-dashboard" />
|
||||
</header>
|
||||
<PlausibleWeb.Live.Components.Form.input
|
||||
name="embed-link"
|
||||
id="embed-link"
|
||||
label="Enter Shared Link (only public shared links without password can be embedded)"
|
||||
value=""
|
||||
width="w-1/2"
|
||||
/>
|
||||
|
||||
<div class="max-w-xl mt-4">
|
||||
<div>
|
||||
<label for="embed-link" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Enter Shared Link
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-200">
|
||||
Only public shared links without password protection can be embedded
|
||||
</p>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
name="embed-link"
|
||||
id="embed-link"
|
||||
onclick="this.select()"
|
||||
class="block w-full border-gray-300 dark:border-gray-700 rounded-md focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PlausibleWeb.Live.Components.Form.input
|
||||
type="select"
|
||||
name="theme"
|
||||
id="theme"
|
||||
label="Select Theme"
|
||||
options={["Light", "Dark", "System"]}
|
||||
value="Light"
|
||||
width="w-1/2"
|
||||
/>
|
||||
|
||||
<div class="mt-4">
|
||||
<label for="theme" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Select Theme
|
||||
</label>
|
||||
<select
|
||||
id="theme"
|
||||
name="theme"
|
||||
class="block w-full py-2 pl-3 pr-10 mt-1 text-base border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-900 dark:text-gray-300"
|
||||
<PlausibleWeb.Live.Components.Form.input
|
||||
name="background"
|
||||
id="background"
|
||||
label="Custom Background Colour (optional). Try using `transparent` background to blend the dashboard with your site."
|
||||
value=""
|
||||
placeholder="e.g. #F9FAFB"
|
||||
width="w-1/2"
|
||||
/>
|
||||
|
||||
<PlausibleWeb.Live.Components.Form.input
|
||||
name="base-url"
|
||||
type="hidden"
|
||||
id="base-url"
|
||||
value={plausible_url()}
|
||||
/>
|
||||
<PlausibleWeb.Components.Generic.button id="generate-embed" class="mt-4">
|
||||
Generate Embed Code
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
|
||||
<%= label(nil, "embed-code", "Embed Code",
|
||||
class: "mt-8 mb-2 block font-medium dark:text-gray-100"
|
||||
) %>
|
||||
|
||||
<div class="relative mt-1">
|
||||
<textarea
|
||||
id="embed-code"
|
||||
name="embed-code"
|
||||
rows="6"
|
||||
readonly="readonly"
|
||||
onclick="this.select()"
|
||||
class="block w-full border-gray-300 dark:border-gray-700 resize-none text-sm shadow-sm focus:ring-indigo-500 focus:border-indigo-500 rounded-md dark:bg-gray-900 dark:text-gray-300"
|
||||
></textarea>
|
||||
<a
|
||||
onclick="var textarea = document.getElementById('embed-code'); textarea.focus(); textarea.select(); document.execCommand('copy');"
|
||||
href="javascript:void(0)"
|
||||
class="text-sm text-indigo-500 no-underline hover:underline"
|
||||
>
|
||||
<option selected>Light</option>
|
||||
<option>Dark</option>
|
||||
<option>System</option>
|
||||
</select>
|
||||
<Heroicons.document_duplicate class="h-5 w-5 absolute text-indigo-700 top-3 right-3" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label for="background" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Custom Background Colour (optional)
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-200">
|
||||
Hint: try using `transparent` background to blend the dashboard with your site background
|
||||
</p>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
name="background"
|
||||
id="background"
|
||||
class="block w-full border-gray-300 dark:border-gray-700 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-900 dark:text-gray-300"
|
||||
placeholder="#F9FAFB"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="base-url" value={plausible_url()} />
|
||||
<PlausibleWeb.Components.Generic.button id="generate-embed" class="mt-4">
|
||||
Generate Embed Code 👇
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="max-w-xl">
|
||||
<label for="embed-code" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Embed Code
|
||||
</label>
|
||||
|
||||
<div class="relative mt-1">
|
||||
<textarea
|
||||
id="embed-code"
|
||||
name="embed-code"
|
||||
rows="3"
|
||||
readonly="readonly"
|
||||
onclick="this.select()"
|
||||
class="block w-full max-w-xl border-gray-300 dark:border-gray-700 resize-none shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-900 dark:text-gray-300"
|
||||
>
|
||||
</textarea>
|
||||
<a
|
||||
onclick="var textarea = document.getElementById('embed-code'); textarea.focus(); textarea.select(); document.execCommand('copy');"
|
||||
href="javascript:void(0)"
|
||||
class="text-sm text-indigo-500 no-underline hover:underline"
|
||||
>
|
||||
<svg
|
||||
class="absolute text-indigo-800"
|
||||
style="top: 12px; right: 12px;"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</.tile>
|
||||
</.settings_tiles>
|
||||
|
@ -1,133 +0,0 @@
|
||||
<div class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
<%= @heading %>
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
<%= @subtitle %>
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="traffic-spikes" />
|
||||
</header>
|
||||
|
||||
<div class="my-8 flex items-center">
|
||||
<%= if @notification do %>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/traffic-change-notification/#{@type}/disable", method: :post, class: "bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %>
|
||||
<span class="translate-x-5 inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-800 shadow transform transition ease-in-out duration-200">
|
||||
</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/traffic-change-notification/#{@type}/enable", method: :post, class: "bg-gray-200 dark:bg-gray-700 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %>
|
||||
<span class="translate-x-0 inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-800 shadow transform transition ease-in-out duration-200">
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<span class="ml-2 dark:text-gray-100"><%= @toggle_text %></span>
|
||||
</div>
|
||||
|
||||
<%= if @notification do %>
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300 mt-6">
|
||||
<%= form_for Plausible.Site.TrafficChangeNotification.changeset(@notification, %{}), "/sites/#{URI.encode_www_form(@site.domain)}/traffic-change-notification/#{@type}", fn f -> %>
|
||||
<h4 class="font-bold my-2"><%= @threshold_label %></h4>
|
||||
<div class="mt-1 flex rounded-md shadow-sm max-w-md">
|
||||
<div class="relative flex items-stretch flex-grow focus-within:z-10">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<!-- Heroicon name: users -->
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<%= number_input(f, :threshold,
|
||||
min: 1,
|
||||
class:
|
||||
"focus:ring-indigo-500 dark:bg-gray-900 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500 dark:text-gray-100"
|
||||
) %>
|
||||
</div>
|
||||
<button class="-ml-px relative button rounded-l-none">
|
||||
<span>Save threshold</span>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<h4 class="font-bold mt-6 dark:text-gray-100">Notification recipients</h4>
|
||||
<%= for recipient <- @notification.recipients do %>
|
||||
<div class="p-2 pl-3 flex justify-between bg-gray-100 dark:bg-gray-900 rounded my-2 max-w-md">
|
||||
<span>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400 inline mr-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
<%= recipient %>
|
||||
</span>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/traffic-change-notification/#{@type}/recipients/#{recipient}", method: :delete) do %>
|
||||
<svg
|
||||
class="w-4 h-4 text-red-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= form_for @conn, "/sites/#{URI.encode_www_form(@site.domain)}/traffic-change-notification/#{@type}/recipients", fn f -> %>
|
||||
<div class="max-w-md mt-4">
|
||||
<div class="mt-1 flex rounded-md shadow-sm">
|
||||
<div class="relative flex items-stretch flex-grow focus-within:z-10">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
</div>
|
||||
<%= email_input(f, :recipient,
|
||||
class:
|
||||
"focus:ring-indigo-500 dark:bg-gray-900 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500 dark:placeholder-gray-400 dark:text-gray-100",
|
||||
placeholder: "recipient@example.com",
|
||||
required: "true"
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<%= submit class: "-ml-px relative button rounded-l-none" do %>
|
||||
<svg
|
||||
class="w-5 h-5 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M8 9a3 3 0 100-6 3 3 0 000 6zM8 11a6 6 0 016 6H2a6 6 0 016-6zM16 7a1 1 0 10-2 0v1h-1a1 1 0 100 2h1v1a1 1 0 102 0v-1h1a1 1 0 100-2h-1V7z">
|
||||
</path>
|
||||
</svg>
|
||||
<span>Add recipient</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
@ -701,7 +701,6 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||
"https://accounts.google.com/o/oauth2/"
|
||||
|
||||
assert resp =~ "Import Data"
|
||||
assert resp =~ "Existing Imports"
|
||||
assert resp =~ "There are no imports yet"
|
||||
assert resp =~ "Export Data"
|
||||
end
|
||||
@ -716,18 +715,18 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||
_site_import4 = insert(:site_import, site: site, status: SiteImport.failed())
|
||||
|
||||
populate_stats(site, site_import3.id, [
|
||||
build(:imported_visitors, pageviews: 77),
|
||||
build(:imported_visitors, pageviews: 21)
|
||||
build(:imported_visitors, pageviews: 7777),
|
||||
build(:imported_visitors, pageviews: 2221)
|
||||
])
|
||||
|
||||
conn = get(conn, "/#{site.domain}/settings/imports-exports")
|
||||
resp = html_response(conn, 200)
|
||||
|
||||
buttons = find(resp, ~s|button[data-method="delete"]|)
|
||||
buttons = find(resp, ~s|a[data-method="delete"]|)
|
||||
assert length(buttons) == 4
|
||||
|
||||
assert resp =~ "Google Analytics (123456)"
|
||||
assert resp =~ "(98 page views)"
|
||||
assert resp =~ "9.9k"
|
||||
end
|
||||
|
||||
test "disables import buttons when imports are at maximum", %{conn: conn, site: site} do
|
||||
@ -747,13 +746,15 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||
insert(:site_import, site: site, legacy: true, status: SiteImport.completed())
|
||||
|
||||
populate_stats(site, [
|
||||
build(:imported_visitors, pageviews: 77),
|
||||
build(:imported_visitors, pageviews: 21)
|
||||
build(:imported_visitors, pageviews: 7777),
|
||||
build(:imported_visitors, pageviews: 2221)
|
||||
])
|
||||
|
||||
conn = get(conn, "/#{site.domain}/settings/imports-exports")
|
||||
|
||||
assert html_response(conn, 200) =~ "(98 page views)"
|
||||
resp = html_response(conn, 200)
|
||||
|
||||
assert resp =~ "9.9k"
|
||||
end
|
||||
|
||||
test "disables import buttons when there's import in progress", %{conn: conn, site: site} do
|
||||
@ -820,7 +821,7 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||
@tag capture_log: true, ee_only: true
|
||||
test "displays error message", %{conn: conn, site: site} do
|
||||
assert conn |> get("/#{site.domain}/settings/imports-exports") |> html_response(200) =~
|
||||
"Something went wrong when fetching exports. Please try again later."
|
||||
"Something went wrong when fetching exports"
|
||||
end
|
||||
end
|
||||
|
||||
@ -1401,7 +1402,7 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||
test "shows form for new shared link", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/sites/#{site.domain}/shared-links/new")
|
||||
|
||||
assert html_response(conn, 200) =~ "New shared link"
|
||||
assert html_response(conn, 200) =~ "New Shared Link"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -300,7 +300,7 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
|
||||
lv = get_liveview(conn, site)
|
||||
|
||||
lv
|
||||
|> element(~s/a[phx-click="edit-funnel"][phx-value-funnel-id=#{f1_id}]/)
|
||||
|> element(~s/button[phx-click="edit-funnel"][phx-value-funnel-id=#{f1_id}]/)
|
||||
|> render_click()
|
||||
|
||||
assert lv = find_live_child(lv, "funnels-form")
|
||||
@ -321,7 +321,7 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
|
||||
lv = get_liveview(conn, site)
|
||||
|
||||
lv
|
||||
|> element(~s/a[phx-click="edit-funnel"][phx-value-funnel-id=#{f1_id}]/)
|
||||
|> element(~s/button[phx-click="edit-funnel"][phx-value-funnel-id=#{f1_id}]/)
|
||||
|> render_click()
|
||||
|
||||
assert lv = find_live_child(lv, "funnels-form")
|
||||
@ -366,7 +366,7 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
|
||||
lv |> element("li#dropdown-step-2-option-1 a") |> render_click()
|
||||
|
||||
doc = lv |> element("#step-eval-0") |> render()
|
||||
assert text_of_element(doc, ~s/#step-eval-0/) =~ "Entering Visitors: 0"
|
||||
assert text_of_element(doc, ~s/#step-eval-0/) =~ "Visitors: 0"
|
||||
|
||||
doc = lv |> element("#step-eval-1") |> render()
|
||||
assert text_of_element(doc, ~s/#step-eval-1/) =~ "Dropoff: 0%"
|
||||
|
@ -22,7 +22,7 @@ defmodule PlausibleWeb.Live.GoalSettingsTest do
|
||||
assert resp =~ to_string(g2)
|
||||
assert resp =~ "Custom Event"
|
||||
assert resp =~ to_string(g3)
|
||||
assert resp =~ "Revenue Goal"
|
||||
assert resp =~ "Revenue Goal (EUR)"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
|
@ -13,7 +13,7 @@ defmodule PlausibleWeb.Live.Shields.CountriesTest do
|
||||
conn = get(conn, "/#{site.domain}/settings/shields/countries")
|
||||
resp = html_response(conn, 200)
|
||||
|
||||
assert resp =~ "No Country Rules configured for this Site"
|
||||
assert resp =~ "No Country Rules configured for this site"
|
||||
assert resp =~ "Country Block List"
|
||||
end
|
||||
|
||||
@ -98,10 +98,10 @@ defmodule PlausibleWeb.Live.Shields.CountriesTest do
|
||||
|
||||
added_by = "#{user.name} <#{user.email}>"
|
||||
|
||||
assert [%{country_code: "EE", added_by: ^added_by}] =
|
||||
assert [%{id: id, country_code: "EE", added_by: ^added_by}] =
|
||||
Shields.list_country_rules(site)
|
||||
|
||||
tooltip = text_of_element(html, ".tooltip-content")
|
||||
tooltip = text_of_attr(html, "#country-#{id}", "title")
|
||||
assert tooltip =~ "Added at #{Date.utc_today()}"
|
||||
assert tooltip =~ "by #{added_by}"
|
||||
end
|
||||
|
@ -13,7 +13,7 @@ defmodule PlausibleWeb.Live.Shields.HostnamesTest do
|
||||
conn = get(conn, "/#{site.domain}/settings/shields/hostnames")
|
||||
resp = html_response(conn, 200)
|
||||
|
||||
assert resp =~ "No Hostname Rules configured for this Site"
|
||||
assert resp =~ "No Hostname Rules configured for this site"
|
||||
assert resp =~ "Hostnames Allow List"
|
||||
assert resp =~ "Traffic from all hostnames is currently accepted."
|
||||
end
|
||||
|
@ -13,7 +13,7 @@ defmodule PlausibleWeb.Live.Shields.IPAddressesTest do
|
||||
conn = get(conn, "/#{site.domain}/settings/shields/ip_addresses")
|
||||
resp = html_response(conn, 200)
|
||||
|
||||
assert resp =~ "No IP Rules configured for this Site"
|
||||
assert resp =~ "No IP Rules configured for this site"
|
||||
assert resp =~ "IP Block List"
|
||||
end
|
||||
|
||||
@ -139,9 +139,9 @@ defmodule PlausibleWeb.Live.Shields.IPAddressesTest do
|
||||
assert html =~ ip
|
||||
assert html =~ user.name
|
||||
|
||||
assert [%{id: _id}] = Shields.list_ip_rules(site)
|
||||
assert [%{id: id}] = Shields.list_ip_rules(site)
|
||||
|
||||
tooltip = text_of_element(html, ".tooltip-content")
|
||||
tooltip = text_of_attr(html, "#inet-#{id}", "title")
|
||||
assert tooltip =~ "Added at #{Date.utc_today()}"
|
||||
assert tooltip =~ "by #{user.name} <#{user.email}>"
|
||||
|
||||
|
@ -13,7 +13,7 @@ defmodule PlausibleWeb.Live.Shields.PagesTest do
|
||||
conn = get(conn, "/#{site.domain}/settings/shields/pages")
|
||||
resp = html_response(conn, 200)
|
||||
|
||||
assert resp =~ "No Page Rules configured for this Site"
|
||||
assert resp =~ "No Page Rules configured for this site"
|
||||
assert resp =~ "Pages Block List"
|
||||
end
|
||||
|
||||
|
@ -10,7 +10,7 @@ defmodule PlausibleWeb.Live.VerificationTest do
|
||||
@retry_button ~s|a[phx-click="retry"]|
|
||||
# @go_to_dashboard_button ~s|a[href$="?skip_to_dashboard=true"]|
|
||||
@progress ~s|#progress-indicator p#progress|
|
||||
@heading ~s|#progress-indicator h3|
|
||||
@heading ~s|#progress-indicator h2|
|
||||
|
||||
describe "GET /:domain" do
|
||||
@tag :ee_only
|
||||
@ -171,16 +171,6 @@ defmodule PlausibleWeb.Live.VerificationTest do
|
||||
|
||||
defp kick_off_live_verification(conn, site) do
|
||||
{:ok, lv, _html} = conn |> no_slowdown() |> no_delay() |> live("/#{site.domain}/verification")
|
||||
|
||||
# {:ok, lv, _} =
|
||||
# live_isolated(conn, PlausibleWeb.Live.Verification,
|
||||
# session: %{
|
||||
# "domain" => site.domain,
|
||||
# "delay" => 0,
|
||||
# "slowdown" => 0
|
||||
# }
|
||||
# )
|
||||
#
|
||||
{:ok, lv}
|
||||
end
|
||||
|
||||
|
Reference in New Issue
Block a user