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:
hq1
2024-10-02 11:05:21 +02:00
committed by GitHub
parent de04f222fd
commit ec2a560016
57 changed files with 2113 additions and 2535 deletions

View File

@ -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}>

View File

@ -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.

View File

@ -13,6 +13,8 @@ defmodule PlausibleWeb do
alias PlausibleWeb.Router.Helpers, as: Routes
alias Phoenix.LiveView.JS
import PlausibleWeb.Components.Generic
end
end

View File

@ -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"
]}

View File

@ -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}
>

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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">

View File

@ -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)}

View File

@ -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>
"""

View File

@ -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?}

View File

@ -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 %>.&nbsp;</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"

View File

@ -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) %>)

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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.
"""

View File

@ -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"

View File

@ -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>
"""

View File

@ -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>

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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 %>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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%"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}>"

View File

@ -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

View File

@ -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