Integrations Settings section (#3427)

* Extend the Tokens context module

* Extract GA Import to separate component

* Extract Search Console settings to separate component

* Remove Search Console from the router

* Stop counting imported pageviews in general settings

* Remove search console controller action

* Add settings_integrations controller action

* Fix remaining redirects

* Add Integrations route

* Replace SC sidebar item with Integrations

* Update site controller tests

* Implement Plugins API Tokens LV

* Apply universal heroicon to docs info links

* Add flash on token creation

* Update CHANGELOG

* Redirect to integrations upon forgetting GA import

* Update moduledocs

* Remove unnecessary wildcards

* WIP: attempt at fixing broken oauth flow

* Fix post-import redirect

* Fixup missing attribute

* Format

* Seed random google auth

* Use example.com for seeded e-mails

* Tweak Google integrations layout

* Remove dangling IO.inspect

* Bugfix: copy to clipboard breaking LV form bindings

* Update lib/plausible/plugins/api/tokens.ex

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>

* Update lib/plausible_web/controllers/site_controller.ex

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>

* Update lib/plausible_web/live/plugins/api/settings.ex

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>

* Update test/plausible/plugins/api/tokens_test.exs

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>

---------

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>
This commit is contained in:
hq1 2023-10-18 14:01:17 +02:00 committed by GitHub
parent 127a9ef9ba
commit 2cc80ebd7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1597 additions and 509 deletions

View File

@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
- Improve password validation in registration and password reset forms
- Adds Gravatar profile image to navbar
- Enforce email reverification on update
- Add Plugins API Tokens provisioning UI
### Removed
- Removed the nested custom event property breakdown UI when filtering by a goal in Goal Conversions
@ -20,6 +21,7 @@ All notable changes to this project will be documented in this file.
- Limit the number of Goal Conversions shown on the dashboard and render a "Details" link when there are more entries to show
- Show Outbound Links / File Downloads / 404 Pages / Cloaked Links instead of Goal Conversions when filtering by the corresponding goal
- Require custom properties to be explicitly added from Site Settings > Custom Properties in order for them to show up on the dashboard
- GA/SC sections moved to new settings: Integrations
### Fixed
- Only return `(none)` values in custom property breakdown for the first page (pagination) of results

View File

@ -36,4 +36,20 @@ defmodule Plausible.Plugins.API.Tokens do
{:error, :not_found}
end
end
@spec delete(Site.t(), String.t()) :: :ok
def delete(site, token_id) do
Repo.delete_all(from t in Token, where: t.site_id == ^site.id and t.id == ^token_id)
:ok
end
@spec list(Site.t()) :: {:ok, [Token.t()]}
def list(site) do
Repo.all(from t in Token, where: t.site_id == ^site.id, order_by: [desc: t.id])
end
@spec any?(Site.t()) :: boolean()
def any?(site) do
Repo.exists?(from(t in Token, where: t.site_id == ^site.id))
end
end

View File

@ -4,10 +4,20 @@ defmodule PlausibleWeb.Components.Generic do
"""
use Phoenix.Component
attr :title, :string, default: "Notice"
attr :size, :atom, default: :sm
attr :rest, :global
slot :inner_block
attr(:slug, :string, required: true)
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 text-gray-400" />
</a>
"""
end
attr(:title, :string, default: "Notice")
attr(:size, :atom, default: :sm)
attr(:rest, :global)
slot(:inner_block)
def notice(assigns) do
~H"""

View File

@ -0,0 +1,65 @@
defmodule PlausibleWeb.Components.Google do
@moduledoc """
Google-related components
"""
use Phoenix.Component
use Phoenix.HTML
attr(:to, :string, required: true)
attr(:id, :string, required: true)
def button(assigns) do
~H"""
<%= button(id: @id, to: @to, class: "inline-flex pr-4 items-center border border-gray-100 shadow rounded-md focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-gray-200 mt-8 hover:bg-gray-50 dark:hover:bg-gray-700") do %>
<.logo />
<span
style="font-family: Roboto, system-ui"
class="text-sm font-medium text-gray-600 dark:text-gray-50"
>
Continue with Google
</span>
<% end %>
"""
end
def logo(assigns \\ %{}) do
~H"""
<svg
width="46px"
height="46px"
viewBox="0 0 46 46"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-1.000000, -1.000000)">
<g transform="translate(15.000000, 15.000000)">
<path
d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z"
fill="#4285F4"
>
</path>
<path
d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z"
fill="#34A853"
>
</path>
<path
d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z"
fill="#FBBC05"
>
</path>
<path
d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z"
fill="#EA4335"
>
</path>
<path d="M0,0 L18,0 L18,18 L0,18 L0,0 Z"></path>
</g>
</g>
</g>
</svg>
"""
end
end

View File

@ -0,0 +1,12 @@
defmodule PlausibleWeb.Components.Settings do
@moduledoc """
An umbrella module for the Integrations settings section
"""
use Phoenix.Component
use Phoenix.HTML
import PlausibleWeb.Components.Generic
embed_templates("../templates/site/settings_search_console.html")
embed_templates("../templates/site/settings_google_import.html")
end

View File

@ -516,7 +516,7 @@ defmodule PlausibleWeb.AuthController do
site = Repo.get(Plausible.Site, site_id)
redirect(conn, to: "/#{URI.encode_www_form(site.domain)}/settings/#{redirect_to}")
redirect(conn, to: "/#{URI.encode_www_form(site.domain)}/settings/integrations")
end
end
end

View File

@ -166,17 +166,9 @@ defmodule PlausibleWeb.SiteController do
conn.assigns[:site]
|> Repo.preload([:custom_domain])
imported_pageviews =
if site.imported_data do
Plausible.Stats.Clickhouse.imported_pageview_count(site)
else
0
end
conn
|> render("settings_general.html",
site: site,
imported_pageviews: imported_pageviews,
changeset: Plausible.Site.changeset(site, %{}),
dogfood_page_path: "/:dashboard/settings/general",
layout: {PlausibleWeb.LayoutView, "site_settings.html"}
@ -251,25 +243,6 @@ defmodule PlausibleWeb.SiteController do
)
end
def settings_search_console(conn, _params) do
site =
conn.assigns[:site]
|> Repo.preload([:google_auth, :custom_domain])
search_console_domains =
if site.google_auth do
Plausible.Google.Api.fetch_verified_properties(site.google_auth)
end
conn
|> render("settings_search_console.html",
site: site,
search_console_domains: search_console_domains,
dogfood_page_path: "/:dashboard/settings/search-console",
layout: {PlausibleWeb.LayoutView, "site_settings.html"}
)
end
def settings_email_reports(conn, _params) do
site = conn.assigns[:site] |> Repo.preload(:custom_domain)
@ -308,6 +281,37 @@ defmodule PlausibleWeb.SiteController do
)
end
def settings_integrations(conn, _params) do
site =
conn.assigns.site
|> Repo.preload([:google_auth, :custom_domain])
search_console_domains =
if site.google_auth do
Plausible.Google.Api.fetch_verified_properties(site.google_auth)
end
imported_pageviews =
if site.imported_data do
Plausible.Stats.Clickhouse.imported_pageview_count(site)
else
0
end
has_plugins_tokens? = Plausible.Plugins.API.Tokens.any?(site)
conn
|> render("settings_integrations.html",
site: site,
imported_pageviews: imported_pageviews,
has_plugins_tokens?: has_plugins_tokens?,
search_console_domains: search_console_domains,
dogfood_page_path: "/:dashboard/settings/integrations",
connect_live_socket: true,
layout: {PlausibleWeb.LayoutView, "site_settings.html"}
)
end
def update_google_auth(conn, %{"google_auth" => attrs}) do
site = conn.assigns[:site] |> Repo.preload(:google_auth)
@ -316,7 +320,7 @@ defmodule PlausibleWeb.SiteController do
conn
|> put_flash(:success, "Google integration saved successfully")
|> redirect(to: Routes.site_path(conn, :settings_search_console, site.domain))
|> redirect(to: Routes.site_path(conn, :settings_integrations, site.domain))
end
def delete_google_auth(conn, _params) do
@ -328,19 +332,7 @@ defmodule PlausibleWeb.SiteController do
conn = put_flash(conn, :success, "Google account unlinked from Plausible")
panel =
conn.path_info
|> List.last()
|> String.split("-")
|> List.last()
case panel do
"search" ->
redirect(conn, to: Routes.site_path(conn, :settings_search_console, site.domain))
"import" ->
redirect(conn, to: Routes.site_path(conn, :settings_general, site.domain))
end
redirect(conn, to: Routes.site_path(conn, :settings_integrations, site.domain))
end
def update_settings(conn, %{"site" => site_params}) do
@ -862,7 +854,7 @@ defmodule PlausibleWeb.SiteController do
conn
|> put_flash(:success, "Import scheduled. An email will be sent when it completes.")
|> redirect(to: Routes.site_path(conn, :settings_general, site.domain))
|> redirect(to: Routes.site_path(conn, :settings_integrations, site.domain))
end
def forget_imported(conn, _params) do
@ -885,12 +877,12 @@ defmodule PlausibleWeb.SiteController do
conn
|> put_flash(:success, "Imported data has been cleared")
|> redirect(to: Routes.site_path(conn, :settings_general, site.domain))
|> redirect(to: Routes.site_path(conn, :settings_integrations, site.domain))
true ->
conn
|> put_flash(:error, "No data has been imported")
|> redirect(to: Routes.site_path(conn, :settings_general, site.domain))
|> redirect(to: Routes.site_path(conn, :settings_integrations, site.domain))
end
end

View File

@ -76,6 +76,40 @@ defmodule PlausibleWeb.Live.Components.Form do
"""
end
attr(:rest, :global)
attr(:id, :string, required: true)
attr(:class, :string, default: "")
attr(:name, :string, required: true)
attr(:label, :string, required: true)
attr(:value, :string, default: "")
def input_with_clipboard(assigns) do
~H"""
<div class="my-4">
<div class="relative mt-1">
<.input
id={@id}
name={@name}
label={@label}
value={@value}
type="text"
readonly="readonly"
class={[@class, "pr-20"]}
{@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"
style="top: 42px; right: 12px;"
>
<Heroicons.document_duplicate class="pr-1 text-indigo-600 dark:text-indigo-500 w-5 h-5" />COPY
</a>
</div>
</div>
"""
end
attr(:id, :any, default: nil)
attr(:label, :string, default: nil)

View File

@ -0,0 +1,146 @@
defmodule PlausibleWeb.Live.Plugins.API.Settings do
@moduledoc """
LiveView allowing listing, creating and revoking Plugins API tokens.
"""
use Phoenix.LiveView
use Phoenix.HTML
alias Plausible.Sites
alias Plausible.Plugins.API.Tokens
def mount(
_params,
%{"domain" => domain, "current_user_id" => user_id} = session,
socket
) do
socket =
socket
|> assign_new(:site, fn ->
Sites.get_for_user!(user_id, domain, [:owner, :admin, :super_admin])
end)
|> assign_new(:displayed_tokens, fn %{site: site} ->
Tokens.list(site)
end)
{:ok,
assign(socket,
domain: domain,
add_token?: not is_nil(session["new_token"]),
token_description: String.capitalize(session["new_token"] || ""),
current_user_id: user_id
)}
end
def render(assigns) do
~H"""
<.live_component id="embedded_liveview_flash" module={PlausibleWeb.Live.Flash} flash={@flash} />
<%= if @add_token? do %>
<%= live_render(
@socket,
PlausibleWeb.Live.Plugins.API.TokenForm,
id: "token-form",
session: %{
"current_user_id" => @current_user_id,
"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">
<button type="button" phx-click="add-token" class="button">
+ Add Token
</button>
</div>
</div>
<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 tracking-wider text-left text-gray-500 uppercase dark:text-gray-100"
>
Description
</th>
<th
scope="col"
class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-100"
>
Hint
</th>
<th scope="col" class="relative 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 whitespace-nowrap">
<span class="token-description"><%= token.description %></span>
</td>
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-100 whitespace-nowrap font-mono">
**********<%= token.hint %>
</td>
<td class="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
<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>
</div>
</div>
"""
end
def handle_event("add-token", _params, socket) do
{:noreply, assign(socket, :add_token?, true)}
end
def handle_event("revoke-token", %{"token-id" => token_id}, socket) do
:ok = Tokens.delete(socket.assigns.site, token_id)
displayed_tokens = Enum.reject(socket.assigns.displayed_tokens, &(&1.id == token_id))
{:noreply, assign(socket, add_token?: false, displayed_tokens: displayed_tokens)}
end
def handle_info(:cancel_add_token, socket) do
{:noreply, assign(socket, add_token?: false)}
end
def handle_info({:token_added, token}, socket) do
displayed_tokens = [token | socket.assigns.displayed_tokens]
socket = put_flash(socket, :success, "Plugins API Token created successfully")
Process.send_after(self(), :clear_flash, 5000)
{:noreply,
assign(socket,
displayed_tokens: displayed_tokens,
add_token?: false,
token_description: ""
)}
end
def handle_info(:clear_flash, socket) do
{:noreply, clear_flash(socket)}
end
end

View File

@ -0,0 +1,114 @@
defmodule PlausibleWeb.Live.Plugins.API.TokenForm do
@moduledoc """
Live view for the goal creation form
"""
use Phoenix.LiveView
import PlausibleWeb.Live.Components.Form
alias Plausible.Repo
alias Plausible.Sites
alias Plausible.Plugins.API.{Token, Tokens}
def mount(
_params,
%{
"token_description" => token_description,
"current_user_id" => user_id,
"domain" => domain,
"rendered_by" => pid
},
socket
) do
socket =
socket
|> assign_new(:site, fn ->
Sites.get_for_user!(user_id, domain, [:owner, :admin, :super_admin])
end)
token = Token.generate()
form = to_form(Token.insert_changeset(socket.assigns.site, token))
{:ok,
assign(socket,
token_description: token_description,
token: token,
current_user: Repo.get(Plausible.Auth.User, user_id),
form: form,
domain: domain,
rendered_by: pid,
tabs: %{custom_events: true, pageviews: false}
)}
end
def render(assigns) do
~H"""
<div
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity z-50"
phx-window-keydown="cancel-add-token"
phx-key="Escape"
>
</div>
<div class="fixed inset-0 flex items-center justify-center mt-16 z-50 overflow-y-auto overflow-x-hidden">
<div class="w-1/2 h-full">
<.form
:let={f}
for={@form}
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"
phx-submit="save-token"
phx-click-away="cancel-add-token"
>
<h2 class="text-xl font-black dark:text-gray-100 mb-8">Add Token for <%= @domain %></h2>
<.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"
/>
<.input_with_clipboard
id="token-clipboard"
name="token_clipboard"
label="API 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"
/>
<p class="text-sm mt-2 text-gray-500 dark:text-gray-200">
Once created, we will not be able to show the Token again.
Please copy the 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">
<button type="submit" class="button text-base font-bold w-full">
Add Token
</button>
</div>
</.form>
</div>
</div>
"""
end
def handle_event("save-token", %{"token" => %{"description" => description}}, socket) do
case Tokens.create(socket.assigns.site, description, socket.assigns.token) do
{:ok, token, _} ->
send(socket.assigns.rendered_by, {:token_added, token})
{:noreply, socket}
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
def handle_event("cancel-add-token", _value, socket) do
send(socket.assigns.rendered_by, :cancel_add_token)
{:noreply, socket}
end
end

View File

@ -284,10 +284,10 @@ defmodule PlausibleWeb.Router do
get "/:website/settings/properties", SiteController, :settings_props
get "/:website/settings/funnels", SiteController, :settings_funnels
get "/:website/settings/search-console", SiteController, :settings_search_console
get "/:website/settings/email-reports", SiteController, :settings_email_reports
get "/:website/settings/custom-domain", SiteController, :settings_custom_domain
get "/:website/settings/danger-zone", SiteController, :settings_danger_zone
get "/:website/settings/integrations", SiteController, :settings_integrations
put "/:website/settings/features/visibility/:setting",
SiteController,

View File

@ -1,20 +1,23 @@
<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>
<%= link(to: "https://plausible.io/docs/email-reports", target: "_blank", rel: "noferrer") do %>
<svg class="w-6 h-6 absolute top-0 right-0 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<% end %>
<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>
<PlausibleWeb.Components.Generic.docs_info slug="email-reports" />
</header>
<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>
<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>
<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>
@ -25,13 +28,34 @@
<%= 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">
<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 %>
</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>
<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 %>
@ -40,16 +64,35 @@
<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">
<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" %>
<%= 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>
<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>
@ -61,11 +104,13 @@
<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>
<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>
<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>
@ -76,13 +121,34 @@
<%= 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">
<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 %>
</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>
<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 %>
@ -91,16 +157,35 @@
<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">
<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" %>
<%= 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>
<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>
@ -112,21 +197,26 @@
<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">Traffic Spike Notifications</h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">Get notified when your site has unusually high number of current visitors</p>
<%= link(to: "https://plausible.io/docs/traffic-spikes", target: "_blank", rel: "noreferrer") do %>
<svg class="w-6 h-6 absolute top-0 right-0 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<% end %>
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
Traffic Spike Notifications
</h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
Get notified when your site has unusually high number of current visitors
</p>
<PlausibleWeb.Components.Generic.docs_info slug="traffic-spikes" />
</header>
<div class="my-8 flex items-center">
<%= if @spike_notification do %>
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification/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>
<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)}/spike-notification/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>
<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 notifications of traffic spikes</span>
@ -134,35 +224,64 @@
<%= if @spike_notification do %>
<div class="text-sm text-gray-700 dark:text-gray-300 mt-6">
<%= form_for Plausible.Site.SpikeNotification.changeset(@spike_notification, %{}), "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification", fn f -> %>
<h4 class="font-bold my-2">Current visitor threshold</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">
<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, 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" %>
<%= number_input(f, :threshold,
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 %>
<% end %>
<h4 class="font-bold mt-6 dark:text-gray-100">Notification recipients</h4>
<%= for recipient <- @spike_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">
<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 %>
</svg>
<%= recipient %>
</span>
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification/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>
<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 %>
@ -171,16 +290,35 @@
<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">
<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" %>
<%= 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>
<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>

View File

@ -12,21 +12,7 @@
Compose Goals into Funnels
</p>
<%= link(to: "https://plausible.io/docs/funnel-analysis", target: "_blank", rel: "noreferrer") do %>
<svg
class="w-6 h-6 absolute top-0 right-0 text-gray-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"
>
</path>
</svg>
<% end %>
<PlausibleWeb.Components.Generic.docs_info slug="funnel-analysis" />
</header>
<PlausibleWeb.Components.Site.Feature.toggle

View File

@ -1,128 +0,0 @@
<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>
<%= link(to: "https://plausible.io/docs/change-domain-name/", target: "_blank", rel: "noreferrer") do %>
<svg class="w-6 h-6 absolute top-0 right-0 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<% end %>
</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>
<span class="inline-flex rounded-md shadow-sm">
<%= link "Change Domain", to: Routes.site_path(@conn, :change_domain, @site.domain), class: "button" %>
</span>
</div>
</div>
<%= 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>
<%= link(to: "https://plausible.io/docs/general/", target: "_blank", rel: "noreferrer") do %>
<svg class="w-6 h-6 absolute top-0 right-0 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<% end %>
</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>
<span class="inline-flex rounded-md shadow-sm">
<%= submit "Save", class: "button" %>
</span>
</div>
</div>
<% end %>
<%= form_for @conn, "/", [class: "shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6"], fn f -> %>
<header class="relative">
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">JavaScript Snippet</h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">Include this Snippet in the <code>&lt;head&gt;</code> of your Website.</p>
<%= link(to: "https://plausible.io/docs/plausible-script", target: "_blank", rel: "noreferrer") do %>
<svg class="w-6 h-6 absolute top-0 right-0 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<% end %>
</header>
<div class="my-4">
<div class="relative">
<code><%= textarea f, :domain, id: "snippet_code", class: "transition overflow-hidden bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 pr-6 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white focus:border-gray-300 dark:focus:border-gray-500 text-xs mt-2 resize-none", value: render_snippet(@site), rows: 2 %></code>
<a onclick="var textarea = document.getElementById('snippet_code'); textarea.focus(); textarea.select(); document.execCommand('copy');" href="javascript:void(0)" class="no-underline text-indigo-500 text-sm hover:underline">
<svg class="absolute text-indigo-500" style="top: 24px; 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>
<% end %>
<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 mt-4">
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Data Import from Google Analytics</h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">Import existing data from your Google Analytics account.</p>
<%= link(to: "https://plausible.io/docs/google-analytics-import", target: "_blank", rel: "noreferrer") do %>
<svg class="w-6 h-6 absolute top-0 right-0 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<% end %>
</header>
<%= if Keyword.get(Application.get_env(:plausible, :google), :client_id) do %>
<%= cond do %>
<% @site.imported_data && @site.imported_data.status == "importing" -> %>
<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">
Import from <%= @site.imported_data.source %>
<svg class="animate-spin -mr-1 ml-1 h-4 w-4 inline text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</p>
<p class="text-sm leading-5 text-gray-500 dark:text-gray-200">
From <%= PlausibleWeb.EmailView.date_format(@site.imported_data.start_date) %> to <%= PlausibleWeb.EmailView.date_format(@site.imported_data.end_date) %>
</p>
</div>
<%= link("Cancel import", to: "/#{URI.encode_www_form(@site.domain)}/settings/forget-imported", 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") %>
</li>
<% @site.imported_data && @site.imported_data.status == "ok" -> %>
<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">
Import from <%= @site.imported_data.source %>
<svg class="h-4 w-4 inline ml-1 -mt-1 text-green-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</p>
<p class="text-sm leading-5 text-gray-500 dark:text-gray-200">
From <%= PlausibleWeb.EmailView.date_format(@site.imported_data.start_date) %> to <%= PlausibleWeb.EmailView.date_format(@site.imported_data.end_date) %>
</p>
</div>
<%= link("Clear " <> PlausibleWeb.StatsView.large_number_format(@imported_pageviews) <> " Imported Pageviews", to: "/#{URI.encode_www_form(@site.domain)}/settings/forget-imported", 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") %>
</li>
<% true -> %>
<%= if @site.imported_data && @site.imported_data.status == "error" do %>
<div class="text-sm mt-2 text-gray-900 dark:text-gray-100">Your latest import has failed. You can try importing again by clicking the button below. If you try multiple times and the import keeps failing, please contact support.</div>
<% end %>
<div class="flex mt-2">
<%= button(to: Plausible.Google.Api.import_authorize_url(@site.id, "import"), class: "inline-flex pr-4 items-center border border-gray-100 shadow rounded-md focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-gray-200 mt-8 hover:bg-gray-50 dark:hover:bg-gray-700") do %>
<%= google_logo() %>
<span style="font-family: Roboto, system-ui" class="text-sm font-medium text-gray-600 dark:text-gray-50">Continue with Google<span>
<% end %>
</div>
<% end %>
<% else %>
<div class="my-8 text-center text-lg">
<svg class="block mx-auto mb-4 w-6 h-6 text-yellow-500" 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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
<p class="text-gray-900 dark:text-gray-200">An extra step is needed to set up your Plausible Analytics Self Hosted for the Google Search Console integration.
Find instructions <%= link("here", to: "https://plausible.io/docs/self-hosting-configuration#google-search-integration", class: "text-indigo-500") %></p>
</div>
<% end %>
</div>

View File

@ -0,0 +1,111 @@
<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>
<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>
<span class="inline-flex rounded-md shadow-sm">
<%= link("Change Domain",
to: Routes.site_path(@conn, :change_domain, @site.domain),
class: "button"
) %>
</span>
</div>
</div>
<%= 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>
<span class="inline-flex rounded-md shadow-sm">
<%= submit("Save", class: "button") %>
</span>
</div>
</div>
<% end %>
<%= form_for @conn, "/", [class: "shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6"], fn f -> %>
<header class="relative">
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
JavaScript Snippet
</h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
Include this Snippet in the <code>&lt;head&gt;</code> of your Website.
</p>
<PlausibleWeb.Components.Generic.docs_info slug="plausible-script" />
</header>
<div class="my-4">
<div class="relative">
<code>
<%= textarea(f, :domain,
id: "snippet_code",
class:
"transition overflow-hidden bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 pr-6 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white focus:border-gray-300 dark:focus:border-gray-500 text-xs mt-2 resize-none",
value: render_snippet(@site),
rows: 2
) %>
</code>
<a
onclick="var textarea = document.getElementById('snippet_code'); textarea.focus(); textarea.select(); document.execCommand('copy');"
href="javascript:void(0)"
class="no-underline text-indigo-500 text-sm hover:underline"
>
<svg
class="absolute text-indigo-500"
style="top: 24px; 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>
<% end %>

View File

@ -11,21 +11,7 @@
>compose Goals into Funnels</a>.
</p>
<%= link(to: "https://plausible.io/docs/goal-conversions", target: "_blank", rel: "noreferrer") do %>
<svg
class="w-6 h-6 absolute top-0 right-0 text-gray-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"
>
</path>
</svg>
<% end %>
<PlausibleWeb.Components.Generic.docs_info slug="goal-conversions" />
</header>
<PlausibleWeb.Components.Site.Feature.toggle

View File

@ -0,0 +1,124 @@
<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 Analytics Data Import
</h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
Import existing data from your Google Analytics account.
</p>
<PlausibleWeb.Components.Generic.docs_info slug="google-analytics-import" />
</header>
<%= if Keyword.get(Application.get_env(:plausible, :google), :client_id) do %>
<%= cond do %>
<% @site.imported_data && @site.imported_data.status == "importing" -> %>
<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">
Import from <%= @site.imported_data.source %>
<svg
class="animate-spin -mr-1 ml-1 h-4 w-4 inline text-indigo-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
>
</circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
>
</path>
</svg>
</p>
<p class="text-sm leading-5 text-gray-500 dark:text-gray-200">
From <%= PlausibleWeb.EmailView.date_format(@site.imported_data.start_date) %> to <%= PlausibleWeb.EmailView.date_format(
@site.imported_data.end_date
) %>
</p>
</div>
<%= link("Cancel import",
to: "/#{URI.encode_www_form(@site.domain)}/settings/forget-imported",
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"
) %>
</li>
<% @site.imported_data && @site.imported_data.status == "ok" -> %>
<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">
Import from <%= @site.imported_data.source %>
<svg
class="h-4 w-4 inline ml-1 -mt-1 text-green-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</p>
<p class="text-sm leading-5 text-gray-500 dark:text-gray-200">
From <%= PlausibleWeb.EmailView.date_format(@site.imported_data.start_date) %> to <%= PlausibleWeb.EmailView.date_format(
@site.imported_data.end_date
) %>
</p>
</div>
<%= link(
"Clear " <>
PlausibleWeb.StatsView.large_number_format(@imported_pageviews) <>
" Imported Pageviews",
to: "/#{URI.encode_www_form(@site.domain)}/settings/forget-imported",
method: :delete,
class:
"inline-block mt-4 px-4 py-2 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"
) %>
</li>
<% true -> %>
<%= if @site.imported_data && @site.imported_data.status == "error" do %>
<div class="text-sm mt-2 text-gray-900 dark:text-gray-100">
Your latest import has failed. You can try importing again by clicking the button below. If you try multiple times and the import keeps failing, please contact support.
</div>
<% end %>
<PlausibleWeb.Components.Google.button
id="analytics-connect"
to={Plausible.Google.Api.import_authorize_url(@site.id, "import")}
/>
<% end %>
<% else %>
<div class="my-8 text-center text-lg">
<svg
class="block mx-auto mb-4 w-6 h-6 text-yellow-500"
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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
>
</path>
</svg>
<p class="text-gray-900 dark:text-gray-200">
An extra step is needed to set up your Plausible Analytics Self Hosted for the Google Search Console integration.
Find instructions <%= link("here",
to: "https://plausible.io/docs/self-hosting-configuration#google-search-integration",
class: "text-indigo-500"
) %>
</p>
</div>
<% end %>
</div>

View File

@ -0,0 +1,32 @@
<PlausibleWeb.Components.Settings.settings_search_console
site={@site}
search_console_domains={@search_console_domains}
/>
<PlausibleWeb.Components.Settings.settings_google_import
site={@site}
imported_pageviews={@imported_pageviews}
/>
<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">
Plugins API Tokens
</h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
Control Plugins API Access
</p>
</header>
<%= live_render(@conn, PlausibleWeb.Live.Plugins.API.Settings,
session: %{
"site_id" => @site.id,
"domain" => @site.domain,
"new_token" => @conn.query_params["new_token"]
}
) %>
</div>
</section>

View File

@ -1,10 +1,11 @@
<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>
<%= link(to: "https://plausible.io/docs/users-roles", target: "_blank", rel: "noreferrer") do %>
<svg class="w-6 h-6 absolute top-0 right-0 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<% end %>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
Invite your friends or coworkers
</p>
<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">
@ -12,7 +13,9 @@
<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") %>
<%= 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">
@ -24,60 +27,125 @@
</div>
<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">
<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"></path></svg>
<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"
>
</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"
>
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>
<p class="mt-1 text-sm text-gray-500">
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
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">
<%= 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") %>
<li
class="select-none hover:bg-gray-100 dark:hover:bg-gray-900 text-red-600"
role="option"
>
<%= 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"
) %>
</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>
<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
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>
<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
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 %>
@ -96,7 +164,9 @@
<%= 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>
<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">
@ -105,10 +175,16 @@
<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">
<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">
<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">
@ -123,10 +199,20 @@
<%= 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 %>
<%= invitation.role |> Atom.to_string() |> String.capitalize() %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<%= link("Remove", to: Routes.invitation_path(@conn, :remove_invitation, @site.domain, invitation.invitation_id), method: :delete, class: "text-red-600 hover:text-red-900") %>
<%= link("Remove",
to:
Routes.invitation_path(
@conn,
:remove_invitation,
@site.domain,
invitation.invitation_id
),
method: :delete,
class: "text-red-600 hover:text-red-900"
) %>
</td>
</tr>
<% end %>
@ -141,7 +227,15 @@
<div class="mt-8">
<%= link(to: Routes.membership_path(@conn, :invite_member_form, @site.domain), class: "button") 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>
<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>
Invite
<% end %>
</div>

View File

@ -7,7 +7,7 @@
/>
<div class="py-6 px-4 sm:p-6">
<header class="w-full flex">
<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
@ -23,25 +23,7 @@
</p>
</span>
<.link
href="https://plausible.io/docs/custom-props/introduction"
target="_blank"
rel="noreferrer"
>
<svg
class="w-6 h-6 text-gray-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"
>
</path>
</svg>
</.link>
<PlausibleWeb.Components.Generic.docs_info slug="custom-props/introduction" />
</header>
<PlausibleWeb.Components.Site.Feature.toggle

View File

@ -1,73 +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">Google Search Console integration</h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">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>
<%= link(to: "https://plausible.io/docs/google-search-console-integration", target: "_blank", rel: "noreferrer") do %>
<svg class="w-6 h-6 absolute top-0 right-0 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<% end %>
</header>
<%= if Keyword.get(Application.get_env(:plausible, :google), :client_id) do %>
<%= if @site.google_auth do %>
<div class="py-2"></div>
<span class="text-gray-700 dark:text-gray-300">Linked Google account: <b><%= @site.google_auth.email %></b></span>
<%= link("Unlink Google account", to: "/#{URI.encode_www_form(@site.domain)}/settings/google-search", 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", method: "delete") %>
<%= 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>
<% else %>
<p class="text-gray-700 dark:text-gray-300 mt-6">
Select the Google Search Console property you would like to pull keyword data from. If you don't see your domain, <%= link("set it up and verify", to: "https://plausible.io/docs/google-search-console-integration", class: "text-indigo-500", target: "_blank", rel: "noreferrer") %> on Search Console first.
</p>
<% 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>
<%= submit "Save", class: "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">
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 %>
<% else %>
<%= button("Continue with Google", to: Plausible.Google.Api.search_console_authorize_url(@site.id, "search-console"), class: "button mt-8") %>
<div class="text-gray-700 dark:text-gray-300 mt-8">
NB: You also need to set up your site on <%= link("Google Search Console", to: "https://search.google.com/search-console/about") %> for the integration to work. <%= link("Read the docs", to: "https://plausible.io/docs/google-search-console-integration", class: "text-indigo-500", rel: "noreferrer") %>
</div>
<% end %>
<% else %>
<div class="my-8 text-center text-lg">
<svg class="block mx-auto mb-4 w-6 h-6 text-yellow-500" 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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
<p class="text-gray-900 dark:text-gray-200">An extra step is needed to set up your Plausible Analytics Self Hosted for the Google Search Console integration.
Find instructions <%= link("here", to: "https://plausible.io/docs/self-hosting-configuration#google-search-integration", class: "text-indigo-500") %></p>
</div>
<% end %>
</div>

View File

@ -0,0 +1,127 @@
<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">
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>
<%= 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>
<%= 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>
<%= 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>
<% else %>
<p class="text-gray-700 dark:text-gray-300 mt-6">
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"
new_tab={true}
>
set it up and verify
</.styled_link>
on Search Console first.
</p>
<% 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>
<%= submit("Save", class: "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">
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 %>
<% else %>
<PlausibleWeb.Components.Google.button
id="search-console-connect"
to={Plausible.Google.Api.search_console_authorize_url(@site.id, "search-console")}
/>
<div class="text-gray-700 dark:text-gray-300 mt-8">
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
</.styled_link>
for the integration to work.
<.styled_link
href="https://plausible.io/docs/google-search-console-integration"
new_tab={true}
>
Read the docs
</.styled_link>,
</div>
<% end %>
<% else %>
<div class="my-8 text-center text-lg">
<svg
class="block mx-auto mb-4 w-6 h-6 text-yellow-500"
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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
>
</path>
</svg>
<p class="text-gray-900 dark:text-gray-200">
An extra step is needed to set up your Plausible Analytics Self Hosted for the Google Search Console integration.
Find instructions <%= link("here",
to: "https://plausible.io/docs/self-hosting-configuration#google-search-integration",
class: "text-indigo-500"
) %>
</p>
</div>
<% end %>
</div>

View File

@ -1,123 +0,0 @@
<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">Share your stats publicly or keep them private</p>
<%= link(to: "https://plausible.io/docs/visibility", target: "_blank", rel: "noreferrer") do %>
<svg class="absolute top-0 right-0 w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<% end %>
</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">
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>
<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">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.</p>
<%= link(to: "https://plausible.io/docs/shared-links", target: "_blank", rel: "noreferrer") do %>
<svg class="absolute top-0 right-0 w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<% end %>
</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">
<%= 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" 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>
<%= 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 %>
<%= link("+ New Link", to: Routes.site_path(@conn, :new_shared_link, @site.domain), class: "button mt-4") %>
</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">Embed Dashboard</h2>
<p class="mt-1 text-sm text-gray-500 leading-5 dark:text-gray-200">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>
<%= link(to: "https://plausible.io/docs/embed-dashboard", target: "_blank", rel: "noreferrer") do %>
<svg class="absolute top-0 right-0 w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<% end %>
</header>
<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>
<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">
<option selected>Light</option>
<option>Dark</option>
<option>System</option>
</select>
</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() %>" />
<button id="generate-embed" class="my-4 button">Generate Embed Code 👇</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>

View File

@ -0,0 +1,266 @@
<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">
Share your stats publicly or keep them private
</p>
<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">
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>
<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">
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.
</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"
>
<%= 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"
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>
<%= 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 %>
<%= link("+ New Link",
to: Routes.site_path(@conn, :new_shared_link, @site.domain),
class: "button mt-4"
) %>
</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">
Embed Dashboard
</h2>
<p class="mt-1 text-sm text-gray-500 leading-5 dark:text-gray-200">
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>
<PlausibleWeb.Components.Generic.docs_info slug="embed-dashboard" />
</header>
<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>
<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"
>
<option selected>Light</option>
<option>Dark</option>
<option>System</option>
</select>
</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()} />
<button id="generate-embed" class="my-4 button">Generate Embed Code 👇</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>

View File

@ -77,7 +77,7 @@ defmodule PlausibleWeb.LayoutView do
[key: "Goals", value: "goals"],
[key: "Funnels", value: "funnels"],
[key: "Custom Properties", value: "properties"],
[key: "Search Console", value: "search-console"],
[key: "Integrations", value: "integrations"],
[key: "Email Reports", value: "email-reports"],
if !is_selfhost() && conn.assigns[:site].custom_domain do
[key: "Custom domain", value: "custom-domain"]

View File

@ -49,6 +49,13 @@ site =
]
)
Plausible.Factory.insert(:google_auth,
user: user,
site: site,
property: "sc-domain:dummy.test",
expires: NaiveDateTime.add(NaiveDateTime.utc_now(), 3600)
)
# Plugins API: on dev environment, use "plausible-plugin-dev-seed-token" for "dummy.site" to authenticate
seeded_token = Plausible.Plugins.API.Token.generate("seed-token")

View File

@ -40,4 +40,37 @@ defmodule Plausible.Plugins.API.TokensTest do
assert {:error, :not_found} = Tokens.find("non-existing")
end
end
describe "any?/2" do
test "returns if a site has any tokens" do
site1 = insert(:site, domain: "foo1.example.com")
site2 = insert(:site, domain: "foo2.example.com")
assert Tokens.any?(site1) == false
assert Tokens.any?(site2) == false
assert {:ok, _, _} = Tokens.create(site1, "My test token")
assert Tokens.any?(site1) == true
assert Tokens.any?(site2) == false
end
end
describe "delete/2" do
test "deletes a token" do
site1 = insert(:site, domain: "foo1.example.com")
site2 = insert(:site, domain: "foo2.example.com")
assert {:ok, t1, _} = Tokens.create(site1, "My test token")
assert {:ok, t2, _} = Tokens.create(site1, "My test token")
assert {:ok, _, _} = Tokens.create(site2, "My test token")
:ok = Tokens.delete(site1, t1.id)
# idempotent
:ok = Tokens.delete(site1, t1.id)
assert Tokens.any?(site1)
:ok = Tokens.delete(site1, t2.id)
refute Tokens.any?(site1)
assert Tokens.any?(site2)
end
end
end

View File

@ -6,6 +6,7 @@ defmodule PlausibleWeb.SiteControllerTest do
import ExUnit.CaptureLog
import Mox
import Plausible.Test.Support.HTML
setup :verify_on_exit!
@ -327,10 +328,8 @@ defmodule PlausibleWeb.SiteControllerTest do
resp = html_response(conn, 200)
assert resp =~ "Site Timezone"
assert resp =~ "Data Import from Google Analytics"
assert resp =~ "https://accounts.google.com/o/oauth2/v2/auth?"
assert resp =~ "analytics.readonly"
refute resp =~ "webmasters.readonly"
assert resp =~ "Site Domain"
assert resp =~ "JavaScript Snippet"
end
end
@ -476,7 +475,7 @@ defmodule PlausibleWeb.SiteControllerTest do
updated_auth = Repo.one(Plausible.Site.GoogleAuth)
assert updated_auth.property == "some-new-property.com"
assert redirected_to(conn, 302) == "/#{site.domain}/settings/search-console"
assert redirected_to(conn, 302) == "/#{site.domain}/settings/integrations"
end
end
@ -488,7 +487,7 @@ defmodule PlausibleWeb.SiteControllerTest do
conn = delete(conn, "/#{site.domain}/settings/google-search")
refute Repo.exists?(Plausible.Site.GoogleAuth)
assert redirected_to(conn, 302) == "/#{site.domain}/settings/search-console"
assert redirected_to(conn, 302) == "/#{site.domain}/settings/integrations"
end
test "fails to delete associated google auth from the outside", %{
@ -504,11 +503,11 @@ defmodule PlausibleWeb.SiteControllerTest do
end
end
describe "GET /:website/settings/search-console for self-hosting" do
describe "GET /:website/settings/integrations for self-hosting" do
setup [:create_user, :log_in, :create_site]
test "display search console settings", %{conn: conn, site: site} do
conn = get(conn, "/#{site.domain}/settings/search-console")
conn = get(conn, "/#{site.domain}/settings/integrations")
resp = html_response(conn, 200)
assert resp =~ "An extra step is needed"
assert resp =~ "Google Search Console integration"
@ -516,7 +515,7 @@ defmodule PlausibleWeb.SiteControllerTest do
end
end
describe "GET /:website/settings/search-console" do
describe "GET /:website/integrations (search-console)" do
setup [:create_user, :log_in, :create_site]
setup_patch_env(:google, client_id: "some", api_url: "https://www.googleapis.com")
@ -529,12 +528,14 @@ defmodule PlausibleWeb.SiteControllerTest do
test "displays Continue with Google link", %{conn: conn, user: user} do
site = insert(:site, domain: "notconnectedyet.example.com", members: [user])
conn = get(conn, "/#{site.domain}/settings/search-console")
conn = get(conn, "/#{site.domain}/settings/integrations")
resp = html_response(conn, 200)
assert resp =~ "Continue with Google"
assert resp =~ "https://accounts.google.com/o/oauth2/v2/auth?"
assert resp =~ "webmasters.readonly"
refute resp =~ "analytics.readonly"
assert button = find(resp, "button#search-console-connect")
assert text(button) == "Continue with Google"
assert text_of_attr(button, "data-to") =~ "https://accounts.google.com/o/oauth2/v2/auth?"
assert text_of_attr(button, "data-to") =~ "webmasters.readonly"
refute text_of_attr(button, "data-to") =~ "analytics.readonly"
end
test "displays appropriate error in case of google account `google_auth_error`", %{
@ -551,7 +552,7 @@ defmodule PlausibleWeb.SiteControllerTest do
end
)
conn = get(conn, "/#{site.domain}/settings/search-console")
conn = get(conn, "/#{site.domain}/settings/integrations")
resp = html_response(conn, 200)
assert resp =~ "Your Search Console account hasn't been connected successfully"
assert resp =~ "Please unlink your Google account and try linking it again"
@ -571,7 +572,7 @@ defmodule PlausibleWeb.SiteControllerTest do
end
)
conn = get(conn, "/#{site.domain}/settings/search-console")
conn = get(conn, "/#{site.domain}/settings/integrations")
resp = html_response(conn, 200)
assert resp =~
@ -592,7 +593,7 @@ defmodule PlausibleWeb.SiteControllerTest do
end
)
conn = get(conn, "/#{site.domain}/settings/search-console")
conn = get(conn, "/#{site.domain}/settings/integrations")
resp = html_response(conn, 200)
assert resp =~ "Something went wrong, but looks temporary"
@ -616,7 +617,7 @@ defmodule PlausibleWeb.SiteControllerTest do
log =
capture_log(fn ->
conn = get(conn, "/#{site.domain}/settings/search-console")
conn = get(conn, "/#{site.domain}/settings/integrations")
resp = html_response(conn, 200)
assert resp =~ "Something went wrong, but looks temporary"

View File

@ -0,0 +1,134 @@
defmodule PlausibleWeb.Live.PluginsAPISettingsTest do
use PlausibleWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Plausible.Test.Support.HTML
alias Plausible.Plugins.API.Tokens
describe "GET /:website/settings/integrations" do
setup [:create_user, :log_in, :create_site]
test "does not display the Plugins API section by default", %{conn: conn, site: site} do
conn = get(conn, "/#{site.domain}/integrations")
resp = html_response(conn, 200)
refute resp =~ "Plugins API Tokens"
end
test "does display the Plugins API section on ?new_token=....", %{
conn: conn,
site: site
} do
conn = get(conn, "/#{site.domain}/settings/integrations?new_token=test")
resp = html_response(conn, 200)
assert resp =~ "Plugins API Tokens"
end
test "does display the Plugins API section when there are tokens already created", %{
conn: conn,
site: site
} do
{:ok, _, _} = Tokens.create(site, "test")
conn = get(conn, "/#{site.domain}/settings/integrations")
resp = html_response(conn, 200)
assert resp =~ "Plugins API Tokens"
end
test "lists tokens with revoke actions", %{conn: conn, site: site} do
{:ok, t1, _} = Tokens.create(site, "test-token-1")
{:ok, t2, _} = Tokens.create(site, "test-token-2")
{:ok, _, _} = Tokens.create(build(:site), "test-token-3")
conn = get(conn, "/#{site.domain}/settings/integrations")
resp = html_response(conn, 200)
assert resp =~ "test-token-1"
assert resp =~ "test-token-2"
assert resp =~ "**********" <> t1.hint
assert resp =~ "**********" <> t2.hint
refute resp =~ "test-token-3"
assert element_exists?(
resp,
~s/button[phx-click="revoke-token"][phx-value-token-id=#{t1.id}]#revoke-token-#{t1.id}/
)
assert element_exists?(
resp,
~s/button[phx-click="revoke-token"][phx-value-token-id=#{t2.id}]#revoke-token-#{t2.id}/
)
end
test "add token button is rendered", %{conn: conn, site: site} do
conn = get(conn, "/#{site.domain}/settings/integrations?new_token=Wordpress")
resp = html_response(conn, 200)
assert element_exists?(resp, ~s/button[phx-click="add-token"]/)
end
end
describe "Plugins.API.Settings live view" do
setup [:create_user, :log_in, :create_site]
test "create token form shows up invoked via URL", %{conn: conn, site: site} do
{_lv, html} =
get_liveview(conn, site, with_html?: true, query_params: "?new_token=Wordpress")
assert element_exists?(html, "#token-form")
assert text_of_element(html, "label[for=token_description]") == "Description"
assert element_exists?(html, "input[value=Wordpress]#token_description")
assert text_of_element(html, "label[for=token-clipboard]") == "API Token"
assert element_exists?(html, "input#token-clipboard")
assert element_exists?(
html,
~s/div#token-form form[phx-submit="save-token"][phx-click-away="cancel-add-token"]/
)
end
test "adds token", %{conn: conn, site: site} do
refute Tokens.any?(site)
lv = get_liveview(conn, site, query_params: "?new_token=Wordpress")
lv
|> find_live_child("token-form")
|> element("form")
|> render_submit()
assert Tokens.any?(site)
html = render(lv)
assert text_of_element(html, "span.token-description") == "Wordpress"
end
test "fails to add token with no description", %{conn: conn, site: site} do
{:ok, _, _} = Tokens.create(site, "test")
lv = get_liveview(conn, site)
lv |> render_click("add-token")
lv
|> find_live_child("token-form")
|> element("form")
|> render_submit()
assert [_] = Tokens.list(site)
end
end
defp get_liveview(conn, site, opts \\ []) do
query_params = Keyword.get(opts, :query_params, "")
conn = assign(conn, :live_module, PlausibleWeb.Live.Plugins.API.Settings)
{:ok, lv, html} = live(conn, "/#{site.domain}/settings/integrations#{query_params}")
if Keyword.get(opts, :with_html?) do
{lv, html}
else
lv
end
end
end

View File

@ -127,7 +127,7 @@ defmodule Plausible.Factory do
def google_auth_factory do
%Plausible.Site.GoogleAuth{
email: sequence(:google_auth_email, &"email-#{&1}@email.com"),
email: sequence(:google_auth_email, &"email-#{&1}@example.com"),
refresh_token: "123",
access_token: "123",
expires: Timex.now() |> Timex.shift(days: 1)