diff --git a/CHANGELOG.md b/CHANGELOG.md index fbb60c66b6..3c23f77f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/plausible/plugins/api/tokens.ex b/lib/plausible/plugins/api/tokens.ex index ceb9b7a871..42f3e0bb49 100644 --- a/lib/plausible/plugins/api/tokens.ex +++ b/lib/plausible/plugins/api/tokens.ex @@ -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 diff --git a/lib/plausible_web/components/generic.ex b/lib/plausible_web/components/generic.ex index f00c227bd2..cf2036823b 100644 --- a/lib/plausible_web/components/generic.ex +++ b/lib/plausible_web/components/generic.ex @@ -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""" + + + + """ + end + + attr(:title, :string, default: "Notice") + attr(:size, :atom, default: :sm) + attr(:rest, :global) + slot(:inner_block) def notice(assigns) do ~H""" diff --git a/lib/plausible_web/components/google.ex b/lib/plausible_web/components/google.ex new file mode 100644 index 0000000000..c8067e00f6 --- /dev/null +++ b/lib/plausible_web/components/google.ex @@ -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 /> + + Continue with Google + + <% end %> + """ + end + + def logo(assigns \\ %{}) do + ~H""" + + + + + + + + + + + + + + + + + + """ + end +end diff --git a/lib/plausible_web/components/settings.ex b/lib/plausible_web/components/settings.ex new file mode 100644 index 0000000000..34f457479c --- /dev/null +++ b/lib/plausible_web/components/settings.ex @@ -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 diff --git a/lib/plausible_web/controllers/auth_controller.ex b/lib/plausible_web/controllers/auth_controller.ex index b33303b824..a1f773503b 100644 --- a/lib/plausible_web/controllers/auth_controller.ex +++ b/lib/plausible_web/controllers/auth_controller.ex @@ -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 diff --git a/lib/plausible_web/controllers/site_controller.ex b/lib/plausible_web/controllers/site_controller.ex index e69a596cde..88fba62ea8 100644 --- a/lib/plausible_web/controllers/site_controller.ex +++ b/lib/plausible_web/controllers/site_controller.ex @@ -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 diff --git a/lib/plausible_web/live/components/form.ex b/lib/plausible_web/live/components/form.ex index 1dd0a4fcf5..f7cbadb4b6 100644 --- a/lib/plausible_web/live/components/form.ex +++ b/lib/plausible_web/live/components/form.ex @@ -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""" +
+
+ <.input + id={@id} + name={@name} + label={@label} + value={@value} + type="text" + readonly="readonly" + class={[@class, "pr-20"]} + {@rest} + /> + + COPY + +
+
+ """ + end + attr(:id, :any, default: nil) attr(:label, :string, default: nil) diff --git a/lib/plausible_web/live/plugins/api/settings.ex b/lib/plausible_web/live/plugins/api/settings.ex new file mode 100644 index 0000000000..40c2b82802 --- /dev/null +++ b/lib/plausible_web/live/plugins/api/settings.ex @@ -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 %> + +
+
+
+ +
+
+ +
+ + + + + + + + + + <%= for token <- @displayed_tokens do %> + + + + + + <% end %> + +
+ Description + + Hint + + Revoke +
+ <%= token.description %> + + **********<%= token.hint %> + + +
+
+
+ """ + 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 diff --git a/lib/plausible_web/live/plugins/api/token_form.ex b/lib/plausible_web/live/plugins/api/token_form.ex new file mode 100644 index 0000000000..0fd31815d3 --- /dev/null +++ b/lib/plausible_web/live/plugins/api/token_form.ex @@ -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""" +
+
+
+
+ <.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" + > +

Add Token for <%= @domain %>

+ + <.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" + /> + +

+ Once created, we will not be able to show the Token again. + Please copy the Token now and store it in a secure place. + + You'll need to paste it in the settings area of the Plausible WordPress plugin. + +

+
+ +
+ +
+
+ """ + 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 diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index 67d9983358..9305f84a68 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -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, diff --git a/lib/plausible_web/templates/site/settings_email_reports.html.eex b/lib/plausible_web/templates/site/settings_email_reports.html.heex similarity index 57% rename from lib/plausible_web/templates/site/settings_email_reports.html.eex rename to lib/plausible_web/templates/site/settings_email_reports.html.heex index 0423d6bb9e..de23058e04 100644 --- a/lib/plausible_web/templates/site/settings_email_reports.html.eex +++ b/lib/plausible_web/templates/site/settings_email_reports.html.heex @@ -1,20 +1,23 @@

Email Reports

-

Send weekly/monthly analytics reports to as many addresses as you wish

- <%= link(to: "https://plausible.io/docs/email-reports", target: "_blank", rel: "noferrer") do %> - - <% end %> +

+ Send weekly/monthly analytics reports to as many addresses as you wish +

+ +
<%= 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 %> - + + <% 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 %> - + + <% end %> <% end %> Send a weekly email report every Monday @@ -25,13 +28,34 @@ <%= for recipient <- @weekly_report.recipients do %>
- + <%= recipient %> <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/weekly-report/recipients/#{recipient}", method: :delete) do %> - + + + + <% end %>
<% end %> @@ -40,16 +64,35 @@
-
- <%= 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" + ) %>
<%= submit class: "-ml-px relative button rounded-l-none" do %> - + + + + Add recipient <% end %>
@@ -61,11 +104,13 @@
<%= 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 %> - + + <% 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 %> - + + <% end %> <% end %> Send a monthly email report on 1st of the month @@ -76,13 +121,34 @@ <%= for recipient <- @monthly_report.recipients do %>
- + <%= recipient %> <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/monthly-report/recipients/#{recipient}", method: :delete) do %> - + + + + <% end %>
<% end %> @@ -91,16 +157,35 @@
-
- <%= 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" + ) %>
<%= submit class: "-ml-px relative button rounded-l-none" do %> - + + + + Add recipient <% end %>
@@ -112,21 +197,26 @@
-

Traffic Spike Notifications

-

Get notified when your site has unusually high number of current visitors

- <%= link(to: "https://plausible.io/docs/traffic-spikes", target: "_blank", rel: "noreferrer") do %> - - <% end %> +

+ Traffic Spike Notifications +

+

+ Get notified when your site has unusually high number of current visitors +

+ +
<%= 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 %> - + + <% 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 %> - + + <% end %> <% end %> Send notifications of traffic spikes @@ -134,35 +224,64 @@ <%= if @spike_notification do %>
- <%= form_for Plausible.Site.SpikeNotification.changeset(@spike_notification, %{}), "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification", fn f -> %>

Current visitor threshold

-
- <%= 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" + ) %>
- <% end %> + <% end %>

Notification recipients

<%= for recipient <- @spike_notification.recipients do %>
- + <%= recipient %> <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification/recipients/#{recipient}", method: :delete) do %> - + + + + <% end %>
<% end %> @@ -171,16 +290,35 @@
-
- <%= 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" + ) %>
<%= submit class: "-ml-px relative button rounded-l-none" do %> - + + + + Add recipient <% end %>
diff --git a/lib/plausible_web/templates/site/settings_funnels.html.heex b/lib/plausible_web/templates/site/settings_funnels.html.heex index 377a147b4b..49bb2dd82d 100644 --- a/lib/plausible_web/templates/site/settings_funnels.html.heex +++ b/lib/plausible_web/templates/site/settings_funnels.html.heex @@ -12,21 +12,7 @@ Compose Goals into Funnels

- <%= link(to: "https://plausible.io/docs/funnel-analysis", target: "_blank", rel: "noreferrer") do %> - - - - - <% end %> + -
-
-

Site Domain

-

Moving your Site to a different Domain? We got you!

- <%= link(to: "https://plausible.io/docs/change-domain-name/", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
-
-
- <%= 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" %> -
-
- - <%= link "Change Domain", to: Routes.site_path(@conn, :change_domain, @site.domain), class: "button" %> - -
-
- -<%= form_for @changeset, "/#{URI.encode_www_form(@site.domain)}/settings", fn f -> %> -
-
-
-

Site Timezone

-

Update your reporting Timezone.

- <%= link(to: "https://plausible.io/docs/general/", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
-
-
- <%= 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" %> -
-
- - <%= submit "Save", class: "button" %> - -
-
-<% 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 -> %> -
-

JavaScript Snippet

-

Include this Snippet in the <head> of your Website.

- - <%= link(to: "https://plausible.io/docs/plausible-script", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
- -
-
- <%= 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 %> - - - -
-
-<% end %> - -
-
-

Data Import from Google Analytics

-

Import existing data from your Google Analytics account.

- <%= link(to: "https://plausible.io/docs/google-analytics-import", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
- - <%= if Keyword.get(Application.get_env(:plausible, :google), :client_id) do %> - <%= cond do %> - <% @site.imported_data && @site.imported_data.status == "importing" -> %> -
  • -
    -

    - Import from <%= @site.imported_data.source %> - - - - -

    -

    - From <%= PlausibleWeb.EmailView.date_format(@site.imported_data.start_date) %> to <%= PlausibleWeb.EmailView.date_format(@site.imported_data.end_date) %> -

    -
    - <%= 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") %> -
  • - - <% @site.imported_data && @site.imported_data.status == "ok" -> %> -
  • -
    -

    - Import from <%= @site.imported_data.source %> - - - -

    -

    - From <%= PlausibleWeb.EmailView.date_format(@site.imported_data.start_date) %> to <%= PlausibleWeb.EmailView.date_format(@site.imported_data.end_date) %> -

    -
    - <%= 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") %> -
  • - - <% true -> %> - <%= if @site.imported_data && @site.imported_data.status == "error" do %> -
    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.
    - <% end %> -
    - <%= 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() %> - Continue with Google - <% end %> -
    - - <% end %> - <% else %> -
    - -

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

    -
    - <% end %> -
    diff --git a/lib/plausible_web/templates/site/settings_general.html.heex b/lib/plausible_web/templates/site/settings_general.html.heex new file mode 100644 index 0000000000..1ef45e133f --- /dev/null +++ b/lib/plausible_web/templates/site/settings_general.html.heex @@ -0,0 +1,111 @@ +
    +
    +
    +

    Site Domain

    +

    + Moving your Site to a different Domain? We got you! +

    + + +
    +
    +
    + <%= 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" + ) %> +
    +
    + + <%= link("Change Domain", + to: Routes.site_path(@conn, :change_domain, @site.domain), + class: "button" + ) %> + +
    +
    + +<%= form_for @changeset, "/#{URI.encode_www_form(@site.domain)}/settings", fn f -> %> +
    +
    +
    +

    + Site Timezone +

    +

    + Update your reporting Timezone. +

    + + +
    +
    +
    + <%= 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" + ) %> +
    +
    + + <%= submit("Save", class: "button") %> + +
    +
    +<% 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 -> %> +
    +

    + JavaScript Snippet +

    +

    + Include this Snippet in the <head> of your Website. +

    + + +
    + +
    +
    + + <%= 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 + ) %> + + + + + + + +
    +
    +<% end %> diff --git a/lib/plausible_web/templates/site/settings_goals.html.heex b/lib/plausible_web/templates/site/settings_goals.html.heex index 191ae29e1f..d987e95c06 100644 --- a/lib/plausible_web/templates/site/settings_goals.html.heex +++ b/lib/plausible_web/templates/site/settings_goals.html.heex @@ -11,21 +11,7 @@ >compose Goals into Funnels.

    - <%= link(to: "https://plausible.io/docs/goal-conversions", target: "_blank", rel: "noreferrer") do %> - - - - - <% end %> + +
    +

    + Google Analytics Data Import +

    +

    + Import existing data from your Google Analytics account. +

    + +
    + + <%= if Keyword.get(Application.get_env(:plausible, :google), :client_id) do %> + <%= cond do %> + <% @site.imported_data && @site.imported_data.status == "importing" -> %> +
  • +
    +

    + Import from <%= @site.imported_data.source %> + + + + + + +

    +

    + From <%= PlausibleWeb.EmailView.date_format(@site.imported_data.start_date) %> to <%= PlausibleWeb.EmailView.date_format( + @site.imported_data.end_date + ) %> +

    +
    + <%= 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" + ) %> +
  • + <% @site.imported_data && @site.imported_data.status == "ok" -> %> +
  • +
    +

    + Import from <%= @site.imported_data.source %> + + + +

    +

    + From <%= PlausibleWeb.EmailView.date_format(@site.imported_data.start_date) %> to <%= PlausibleWeb.EmailView.date_format( + @site.imported_data.end_date + ) %> +

    +
    + <%= 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" + ) %> +
  • + <% true -> %> + <%= if @site.imported_data && @site.imported_data.status == "error" do %> +
    + 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. +
    + <% end %> + + <% end %> + <% else %> +
    + + + + +

    + 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" + ) %> +

    +
    + <% end %> +
    diff --git a/lib/plausible_web/templates/site/settings_integrations.html.heex b/lib/plausible_web/templates/site/settings_integrations.html.heex new file mode 100644 index 0000000000..59acd3b4ea --- /dev/null +++ b/lib/plausible_web/templates/site/settings_integrations.html.heex @@ -0,0 +1,32 @@ + + + +
    +
    +
    +

    + Plugins API Tokens +

    +

    + Control Plugins API Access +

    +
    + + <%= live_render(@conn, PlausibleWeb.Live.Plugins.API.Settings, + session: %{ + "site_id" => @site.id, + "domain" => @site.domain, + "new_token" => @conn.query_params["new_token"] + } + ) %> +
    +
    diff --git a/lib/plausible_web/templates/site/settings_people.html.eex b/lib/plausible_web/templates/site/settings_people.html.heex similarity index 52% rename from lib/plausible_web/templates/site/settings_people.html.eex rename to lib/plausible_web/templates/site/settings_people.html.heex index adc5bcd095..5d57dd6ba2 100644 --- a/lib/plausible_web/templates/site/settings_people.html.eex +++ b/lib/plausible_web/templates/site/settings_people.html.heex @@ -1,10 +1,11 @@

    People

    -

    Invite your friends or coworkers

    - <%= link(to: "https://plausible.io/docs/users-roles", target: "_blank", rel: "noreferrer") do %> - - <% end %> +

    + Invite your friends or coworkers +

    + +
      @@ -12,7 +13,9 @@
    • - <%= 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" + ) %>

      @@ -24,60 +27,125 @@

      -
        + 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 %>
      • Owner

        -

        Site owner cannot be assigned to any other role

        +

        + Site owner cannot be assigned to any other role +

        -
      • <%= if @conn.assigns[:current_user_role] == :owner do %> -
      • - <%= 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") %> +
      • + <%= 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" + ) %>
      • <% 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 %>
        -

        Admin

        -

        View stats and edit site settings

        +

        + Admin +

        +

        + View stats and edit site settings +

        <%= if membership.role == :admin do %> - <% 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 %>
        -

        Viewer

        -

        View stats only

        +

        + Viewer +

        +

        + View stats only +

        <%= if membership.role == :viewer do %> - <% end %> @@ -96,7 +164,9 @@ <%= if Enum.count(@site.invitations) > 0 do %>
        -

        Pending invitations

        +

        + Pending invitations +

        @@ -105,10 +175,16 @@ - - <% end %> @@ -141,7 +227,15 @@
        <%= link(to: Routes.membership_path(@conn, :invite_member_form, @site.domain), class: "button") do %> - + + + + Invite <% end %>
        diff --git a/lib/plausible_web/templates/site/settings_props.html.heex b/lib/plausible_web/templates/site/settings_props.html.heex index 7675efd817..94ba15895b 100644 --- a/lib/plausible_web/templates/site/settings_props.html.heex +++ b/lib/plausible_web/templates/site/settings_props.html.heex @@ -7,7 +7,7 @@ />
        -
        +

        Custom Properties @@ -23,25 +23,7 @@

        - <.link - href="https://plausible.io/docs/custom-props/introduction" - target="_blank" - rel="noreferrer" - > - - - - - +

        -
        -

        Google Search Console integration

        -

        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.

        - <%= link(to: "https://plausible.io/docs/google-search-console-integration", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
        - - <%= if Keyword.get(Application.get_env(:plausible, :google), :client_id) do %> - <%= if @site.google_auth do %> -
        - Linked Google account: <%= @site.google_auth.email %> - - <%= 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 %> -

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

        - <% else %> -

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

        - <% end %> - - <%= form_for Plausible.Site.GoogleAuth.changeset(@site.google_auth), "/#{URI.encode_www_form(@site.domain)}/settings/google", [class: "max-w-xs"], fn f -> %> -
        -
        - <%= 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" %> -
        -
        - - <%= submit "Save", class: "button" %> - <% end %> - <% {:error, error} -> %> -

        The following error happened when fetching your Google Search Console domains:

        - - <%= case error do %> - <% "invalid_grant" -> %> -

        - - Invalid Grant error returned from Google. See here on how to fix it. - -

        - <% "google_auth_error" -> %> -

        - Your Search Console account hasn't been connected successfully. Please unlink your Google account and try linking it again. -

        - - <% _ -> %> -

        - Something went wrong, but looks temporary. If the problem persists, try re-linking your Google account. -

        - <% end %> - <% end %> - <% else %> - <%= button("Continue with Google", to: Plausible.Google.Api.search_console_authorize_url(@site.id, "search-console"), class: "button 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") %> -
        - <% end %> - <% else %> -
        - -

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

        -
        - <% end %> -
        diff --git a/lib/plausible_web/templates/site/settings_search_console.html.heex b/lib/plausible_web/templates/site/settings_search_console.html.heex new file mode 100644 index 0000000000..5b5b6aff9c --- /dev/null +++ b/lib/plausible_web/templates/site/settings_search_console.html.heex @@ -0,0 +1,127 @@ +
        +
        +

        + Google Search Console Integration +

        +

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

        + + +
        + + <%= if Keyword.get(Application.get_env(:plausible, :google), :client_id) do %> + <%= if @site.google_auth do %> +
        + + Linked Google account: <%= @site.google_auth.email %> + + + <%= 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" + ) %> +
        + + <%= case @search_console_domains do %> + <% {:ok, domains} -> %> + <%= if @site.google_auth.property && !(@site.google_auth.property in domains) do %> +

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

        + <% else %> +

        + 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 + + on Search Console first. +

        + <% end %> + + <%= form_for Plausible.Site.GoogleAuth.changeset(@site.google_auth), "/#{URI.encode_www_form(@site.domain)}/settings/google", [class: "max-w-xs"], fn f -> %> +
        +
        + <%= 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" + ) %> +
        +
        + + <%= submit("Save", class: "button") %> + <% end %> + <% {:error, error} -> %> +

        + The following error happened when fetching your Google Search Console domains: +

        + + <%= case error do %> + <% "invalid_grant" -> %> +

        + + Invalid Grant error returned from Google. See here on how to fix it. + +

        + <% "google_auth_error" -> %> +

        + Your Search Console account hasn't been connected successfully. Please unlink your Google account and try linking it again. +

        + <% _ -> %> +

        + Something went wrong, but looks temporary. If the problem persists, try re-linking your Google account. +

        + <% end %> + <% end %> + <% else %> + +
        + 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 + + for the integration to work. + <.styled_link + href="https://plausible.io/docs/google-search-console-integration" + new_tab={true} + > + Read the docs + , +
        + <% end %> + <% else %> +
        + + + + +

        + 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" + ) %> +

        +
        + <% end %> +
        diff --git a/lib/plausible_web/templates/site/settings_visibility.html.eex b/lib/plausible_web/templates/site/settings_visibility.html.eex deleted file mode 100644 index 1f7b86ad7a..0000000000 --- a/lib/plausible_web/templates/site/settings_visibility.html.eex +++ /dev/null @@ -1,123 +0,0 @@ -
        -
        -

        Public dashboard

        -

        Share your stats publicly or keep them private

        - <%= link(to: "https://plausible.io/docs/visibility", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
        - - <%= if @site.public do %> -
        - <%= 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 %> - - <% end %> - - Stats are publicly available on <%= link(PlausibleWeb.StatsView.pretty_stats_url(@site), to: Routes.stats_path(@conn, :stats, @site.domain, []), class: "text-indigo-500") %> - -
        - <% else %> -
        - <%= 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 %> - - <% end %> - - Make stats publicly available on <%= link(PlausibleWeb.StatsView.pretty_stats_url(@site), to: Routes.stats_path(@conn, :stats, @site.domain, []), class: "text-indigo-500") %> - -
        - <% end %> -
        - -
        -
        -

        Shared Links

        -

        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.

        - <%= link(to: "https://plausible.io/docs/shared-links", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
        - -
        - <%= for link <- @shared_links do %> -
        - -
        - - - - <%= 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 %> - - <% 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 %> - - <% end %> -
        -
        - <% end %> - - <%= link("+ New Link", to: Routes.site_path(@conn, :new_shared_link, @site.domain), class: "button mt-4") %> -
        -
        - -
        -
        -

        Embed Dashboard

        -

        You can use shared links to embed your stats in any other webpage using an iframe. Copy & paste a shared link into the form below to generate the embed code.

        - <%= link(to: "https://plausible.io/docs/embed-dashboard", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
        - -
        -
        - -

        Only public shared links without password protection can be embedded

        -
        - -
        -
        - -
        - - -
        - -
        - -

        Hint: try using `transparent` background to blend the dashboard with your site background

        -
        - -
        -
        -
        - - - - -
        -
        - - -
        - - - - -
        -
        -
        -
        diff --git a/lib/plausible_web/templates/site/settings_visibility.html.heex b/lib/plausible_web/templates/site/settings_visibility.html.heex new file mode 100644 index 0000000000..ea73d95f75 --- /dev/null +++ b/lib/plausible_web/templates/site/settings_visibility.html.heex @@ -0,0 +1,266 @@ +
        +
        +

        + Public dashboard +

        +

        + Share your stats publicly or keep them private +

        + + +
        + + <%= if @site.public do %> +
        + <%= 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 %> + + + <% end %> + + Stats are publicly available on <%= link(PlausibleWeb.StatsView.pretty_stats_url(@site), + to: Routes.stats_path(@conn, :stats, @site.domain, []), + class: "text-indigo-500" + ) %> + +
        + <% else %> +
        + <%= 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 %> + + + <% end %> + + Make stats publicly available on <%= link(PlausibleWeb.StatsView.pretty_stats_url(@site), + to: Routes.stats_path(@conn, :stats, @site.domain, []), + class: "text-indigo-500" + ) %> + +
        + <% end %> +
        + +
        +
        +

        Shared Links

        +

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

        + + +
        + +
        + <%= for link <- @shared_links do %> +
        + +
        + + + + <%= 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 %> + + + + + + + <% 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 %> + + + + + <% end %> +
        +
        + <% end %> + + <%= link("+ New Link", + to: Routes.site_path(@conn, :new_shared_link, @site.domain), + class: "button mt-4" + ) %> +
        +
        + +
        +
        +

        + Embed Dashboard +

        +

        + You can use shared links to embed your stats in any other webpage using an iframe. Copy & paste a shared link into the form below to generate the embed code. +

        + + +
        + +
        +
        + +

        + Only public shared links without password protection can be embedded +

        +
        + +
        +
        + +
        + + +
        + +
        + +

        + Hint: try using `transparent` background to blend the dashboard with your site background +

        +
        + +
        +
        +
        + + + + +
        +
        + + +
        + + + + + + + +
        +
        +
        +
        diff --git a/lib/plausible_web/views/layout_view.ex b/lib/plausible_web/views/layout_view.ex index 72f35af97a..7f5527c00b 100644 --- a/lib/plausible_web/views/layout_view.ex +++ b/lib/plausible_web/views/layout_view.ex @@ -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"] diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 4e86826a3c..575bce4045 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -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") diff --git a/test/plausible/plugins/api/tokens_test.exs b/test/plausible/plugins/api/tokens_test.exs index d1be798739..c085a324e0 100644 --- a/test/plausible/plugins/api/tokens_test.exs +++ b/test/plausible/plugins/api/tokens_test.exs @@ -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 diff --git a/test/plausible_web/controllers/site_controller_test.exs b/test/plausible_web/controllers/site_controller_test.exs index e968f90b6e..e6f102a1d6 100644 --- a/test/plausible_web/controllers/site_controller_test.exs +++ b/test/plausible_web/controllers/site_controller_test.exs @@ -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" diff --git a/test/plausible_web/live/plugins_api_tokens_test.exs b/test/plausible_web/live/plugins_api_tokens_test.exs new file mode 100644 index 0000000000..9a509ae337 --- /dev/null +++ b/test/plausible_web/live/plugins_api_tokens_test.exs @@ -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 diff --git a/test/support/factory.ex b/test/support/factory.ex index 9979d29098..7d91eae7fa 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -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)
        + Email + Role @@ -123,10 +199,20 @@ <%= invitation.email %> - <%= invitation.role |> Atom.to_string |> String.capitalize %> + <%= invitation.role |> Atom.to_string() |> String.capitalize() %> - <%= 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" + ) %>