diff --git a/assets/js/liveview/dashboard_root.js b/assets/js/liveview/dashboard_root.js index 769044bc4423..b7b25ad6e391 100644 --- a/assets/js/liveview/dashboard_root.js +++ b/assets/js/liveview/dashboard_root.js @@ -6,17 +6,25 @@ import { buildHook } from './hook_builder' -function navigateWithLoader(url) { - this.portalTargets.map((target) => { - this.js().addClass(document.querySelector(target), 'phx-navigation-loading') +const MODAL_ROUTES = { + '/pages': '#pages-breakdown-details-modal', + '/entry-pages': '#entry-pages-breakdown-details-modal', + '/exit-pages': '#exit-pages-breakdown-details-modal', + '/sources': '#sources-breakdown-details-modal', + '/channels': '#channels-breakdown-details-modal', + '/utm_medium': '#utm-mediums-breakdown-details-modal' +} - this.pushEvent('handle_dashboard_params', { url: url }, () => { - this.js().removeClass( - document.querySelector(target), - 'phx-navigation-loading' - ) - }) - }) +function routeModal(path) { + const modalId = MODAL_ROUTES[path] + + if (modalId) { + const modal = document.querySelector(modalId) + + if (modal) { + modal.dispatchEvent(new Event('prima:modal:open')) + } + } } export default buildHook({ @@ -26,39 +34,16 @@ export default buildHook({ this.url = window.location.href this.addListener('click', document.body, (e) => { - const type = e.target.dataset.type || null + const link = e.target.closest('[data-phx-link]') + const type = link && (link.dataset.type || null) if (type === 'dashboard-link') { - const uri = new URL(e.target.href) + const uri = new URL(link.href) // Domain is dropped from URL prefix, because that's what react-dom-router // expects. const path = '/' + uri.pathname.split('/').slice(2).join('/') - this.el.dispatchEvent( - new CustomEvent('dashboard:live-navigate', { - bubbles: true, - detail: { path: path, search: uri.search } - }) - ) - - e.preventDefault() - } - }) - - // Browser back and forward navigation triggers that event. - this.addListener('popstate', window, () => { - if (this.url !== window.location.href) { - navigateWithLoader.bind(this)(window.location.href) - } - }) - // Navigation events triggered from liveview are propagated via this - // handler. - this.addListener('dashboard:live-navigate-back', window, (e) => { - if ( - typeof e.detail.search === 'string' && - this.url !== window.location.href - ) { - navigateWithLoader.bind(this)(window.location.href) + routeModal(path) } }) } diff --git a/lib/plausible/stats/dashboard/utils.ex b/lib/plausible/stats/dashboard/utils.ex index e3911351f327..54ae75ceb93b 100644 --- a/lib/plausible/stats/dashboard/utils.ex +++ b/lib/plausible/stats/dashboard/utils.ex @@ -19,7 +19,7 @@ defmodule Plausible.Stats.Dashboard.Utils do end end - def dashboard_route(%Site{} = site, %ParsedQueryParams{} = params, opts) do + def dashboard_route(%Site{} = site, %ParsedQueryParams{} = params, opts \\ []) do path = Keyword.get(opts, :path, "") params = diff --git a/lib/plausible_web/live/components/dashboard/base.ex b/lib/plausible_web/live/components/dashboard/base.ex index 096654daf98c..286f6aba7347 100644 --- a/lib/plausible_web/live/components/dashboard/base.ex +++ b/lib/plausible_web/live/components/dashboard/base.ex @@ -5,6 +5,8 @@ defmodule PlausibleWeb.Components.Dashboard.Base do use PlausibleWeb, :component + alias Prima.Modal + attr :to, :string, required: true attr :class, :string, default: "" attr :rest, :global @@ -48,4 +50,73 @@ defmodule PlausibleWeb.Components.Dashboard.Base do """ end + + attr :id, :string, required: true + attr :on_close, JS, default: %JS{} + attr :show, :boolean, default: false + attr :ready, :boolean, default: true + slot :inner_block, required: true + + def modal(assigns) do + ~H""" +
+
+
+
+
+
+
+
+
+ + + +
+
+ +
+
+
+
+ "-panel"} + class="relative overflow-hidden rounded-lg bg-white dark:bg-gray-900 text-left shadow-xl sm:w-full sm:max-w-lg" + transition_enter={ + {"ease-out duration-300", "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", + "opacity-100 translate-y-0 sm:scale-100"} + } + transition_leave={ + {"ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100", + "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"} + } + > + {render_slot(@inner_block)} + +
+
+
+ """ + end + + slot :inner_block, required: true + + def modal_title(assigns) do + ~H""" + + {render_slot(@inner_block)} + + """ + end end diff --git a/lib/plausible_web/live/components/dashboard/tile.ex b/lib/plausible_web/live/components/dashboard/tile.ex index 7268df71621e..a6965f099388 100644 --- a/lib/plausible_web/live/components/dashboard/tile.ex +++ b/lib/plausible_web/live/components/dashboard/tile.ex @@ -37,7 +37,7 @@ defmodule PlausibleWeb.Components.Dashboard.Tile do > {render_slot(@tabs)} -
+
{render_slot(@warnings)}
@@ -45,7 +45,7 @@ defmodule PlausibleWeb.Components.Dashboard.Tile do <%!-- reportbody --%>
@@ -55,7 +55,7 @@ defmodule PlausibleWeb.Components.Dashboard.Tile do
{render_slot(@inner_block)}
@@ -113,8 +113,11 @@ defmodule PlausibleWeb.Components.Dashboard.Tile do defp details_link(assigns) do ~H""" + <.spinner class="group-has-[.phx-click-loading:not([data-skip-loader])]/dashboard:inline group-has-[.tile-tabs.phx-hook-loading]:inline w-4 h-4 hidden" /> +
<.live_component @@ -97,6 +97,61 @@ defmodule PlausibleWeb.Live.Dashboard do connected?={@connected?} params={@params} /> + <.live_component + module={PlausibleWeb.Live.Dashboard.PagesDetails} + id="pages-breakdown-details-component" + site={@site} + user_prefs={@user_prefs} + connected?={@connected?} + params={@params} + open?={@path == ["pages"]} + /> + <.live_component + module={PlausibleWeb.Live.Dashboard.EntryPagesDetails} + id="entry-pages-breakdown-details-component" + site={@site} + user_prefs={@user_prefs} + connected?={@connected?} + params={@params} + open?={@path == ["entry-pages"]} + /> + <.live_component + module={PlausibleWeb.Live.Dashboard.ExitPagesDetails} + id="exit-pages-breakdown-details-component" + site={@site} + user_prefs={@user_prefs} + connected?={@connected?} + params={@params} + open?={@path == ["exit-pages"]} + /> + <.live_component + module={PlausibleWeb.Live.Dashboard.SourcesDetails} + id="sources-breakdown-details-component" + site={@site} + user_prefs={@user_prefs} + connected?={@connected?} + params={@params} + open?={@path == ["sources"]} + /> + <.live_component + module={PlausibleWeb.Live.Dashboard.ChannelsDetails} + id="channels-breakdown-details-component" + site={@site} + user_prefs={@user_prefs} + connected?={@connected?} + params={@params} + open?={@path == ["channels"]} + /> + <.live_component + module={PlausibleWeb.Live.Dashboard.UtmMediumsDetails} + id="utm-mediums-breakdown-details-component" + site={@site} + user_prefs={@user_prefs} + connected?={@connected?} + params={@params} + open?={@path == ["utm_medium"]} + /> +
""" end diff --git a/lib/plausible_web/live/dashboard/channels_details.ex b/lib/plausible_web/live/dashboard/channels_details.ex new file mode 100644 index 000000000000..73bedf865d52 --- /dev/null +++ b/lib/plausible_web/live/dashboard/channels_details.ex @@ -0,0 +1,91 @@ +defmodule PlausibleWeb.Live.Dashboard.ChannelsDetails do + @moduledoc """ + Channels details modal breakdown component. + """ + + use PlausibleWeb, :live_component + + alias Plausible.Stats + alias Plausible.Stats.Dashboard.Utils + alias Plausible.Stats.ParsedQueryParams + alias Plausible.Stats.QueryBuilder + alias Plausible.Stats.QueryResult + alias PlausibleWeb.Components.Dashboard.ReportList + + import PlausibleWeb.Components.Dashboard.Base + + @pagination %{limit: 50, offset: 0} + + def update(assigns, socket) do + close_url = Utils.dashboard_route(assigns.site, assigns.params) + + socket = + assign(socket, + site: assigns.site, + params: assigns.params, + open?: assigns.open?, + connected?: assigns.connected?, + close_url: close_url + ) + |> load_stats() + + {:ok, socket} + end + + def render(assigns) do + ~H""" +
+ <.modal + id="channels-breakdown-details-modal" + on_close={JS.patch(@close_url)} + show={@open?} + ready={@connected?} + > + <.modal_title> + Channels + + +
+ +
+ +
+ """ + end + + defp load_stats(socket) do + %{site: site, params: params} = socket.assigns + + metrics = choose_metrics(params) + + params = + params + |> ParsedQueryParams.set( + metrics: metrics, + dimensions: ["visit:channel"], + pagination: @pagination + ) + + query = QueryBuilder.build!(site, params) + + %QueryResult{} = query_result = Stats.query(site, query) + + assign(socket, :query_result, query_result) + end + + defp choose_metrics(%ParsedQueryParams{} = params) do + if ParsedQueryParams.conversion_goal_filter?(params) do + [:visitors, :group_conversion_rate] + else + [:visitors] + end + end +end diff --git a/lib/plausible_web/live/dashboard/entry_pages_details.ex b/lib/plausible_web/live/dashboard/entry_pages_details.ex new file mode 100644 index 000000000000..f5655f0e4435 --- /dev/null +++ b/lib/plausible_web/live/dashboard/entry_pages_details.ex @@ -0,0 +1,91 @@ +defmodule PlausibleWeb.Live.Dashboard.EntryPagesDetails do + @moduledoc """ + Entry pages details modal breakdown component. + """ + + use PlausibleWeb, :live_component + + alias Plausible.Stats + alias Plausible.Stats.Dashboard.Utils + alias Plausible.Stats.ParsedQueryParams + alias Plausible.Stats.QueryBuilder + alias Plausible.Stats.QueryResult + alias PlausibleWeb.Components.Dashboard.ReportList + + import PlausibleWeb.Components.Dashboard.Base + + @pagination %{limit: 50, offset: 0} + + def update(assigns, socket) do + close_url = Utils.dashboard_route(assigns.site, assigns.params) + + socket = + assign(socket, + site: assigns.site, + params: assigns.params, + open?: assigns.open?, + connected?: assigns.connected?, + close_url: close_url + ) + |> load_stats() + + {:ok, socket} + end + + def render(assigns) do + ~H""" +
+ <.modal + id="entry-pages-breakdown-details-modal" + on_close={JS.patch(@close_url)} + show={@open?} + ready={@connected?} + > + <.modal_title> + Entry Pages + + +
+ +
+ +
+ """ + end + + defp load_stats(socket) do + %{site: site, params: params} = socket.assigns + + metrics = choose_metrics(params) + + params = + params + |> ParsedQueryParams.set( + metrics: metrics, + dimensions: ["visit:entry_page"], + pagination: @pagination + ) + + query = QueryBuilder.build!(site, params) + + %QueryResult{} = query_result = Stats.query(site, query) + + assign(socket, :query_result, query_result) + end + + defp choose_metrics(%ParsedQueryParams{} = params) do + if ParsedQueryParams.conversion_goal_filter?(params) do + [:visitors, :group_conversion_rate] + else + [:visitors] + end + end +end diff --git a/lib/plausible_web/live/dashboard/exit_pages_details.ex b/lib/plausible_web/live/dashboard/exit_pages_details.ex new file mode 100644 index 000000000000..2e7164962a56 --- /dev/null +++ b/lib/plausible_web/live/dashboard/exit_pages_details.ex @@ -0,0 +1,91 @@ +defmodule PlausibleWeb.Live.Dashboard.ExitPagesDetails do + @moduledoc """ + Exit pages details modal breakdown component. + """ + + use PlausibleWeb, :live_component + + alias Plausible.Stats + alias Plausible.Stats.Dashboard.Utils + alias Plausible.Stats.ParsedQueryParams + alias Plausible.Stats.QueryBuilder + alias Plausible.Stats.QueryResult + alias PlausibleWeb.Components.Dashboard.ReportList + + import PlausibleWeb.Components.Dashboard.Base + + @pagination %{limit: 50, offset: 0} + + def update(assigns, socket) do + close_url = Utils.dashboard_route(assigns.site, assigns.params) + + socket = + assign(socket, + site: assigns.site, + params: assigns.params, + open?: assigns.open?, + connected?: assigns.connected?, + close_url: close_url + ) + |> load_stats() + + {:ok, socket} + end + + def render(assigns) do + ~H""" +
+ <.modal + id="exit-pages-breakdown-details-modal" + on_close={JS.patch(@close_url)} + show={@open?} + ready={@connected?} + > + <.modal_title> + Exit Pages + + +
+ +
+ +
+ """ + end + + defp load_stats(socket) do + %{site: site, params: params} = socket.assigns + + metrics = choose_metrics(params) + + params = + params + |> ParsedQueryParams.set( + metrics: metrics, + dimensions: ["visit:exit_page"], + pagination: @pagination + ) + + query = QueryBuilder.build!(site, params) + + %QueryResult{} = query_result = Stats.query(site, query) + + assign(socket, :query_result, query_result) + end + + defp choose_metrics(%ParsedQueryParams{} = params) do + if ParsedQueryParams.conversion_goal_filter?(params) do + [:visitors, :group_conversion_rate] + else + [:visitors] + end + end +end diff --git a/lib/plausible_web/live/dashboard/pages_details.ex b/lib/plausible_web/live/dashboard/pages_details.ex new file mode 100644 index 000000000000..b6f956c05197 --- /dev/null +++ b/lib/plausible_web/live/dashboard/pages_details.ex @@ -0,0 +1,91 @@ +defmodule PlausibleWeb.Live.Dashboard.PagesDetails do + @moduledoc """ + Pages details modal breakdown component. + """ + + use PlausibleWeb, :live_component + + alias Plausible.Stats + alias Plausible.Stats.Dashboard.Utils + alias Plausible.Stats.ParsedQueryParams + alias Plausible.Stats.QueryBuilder + alias Plausible.Stats.QueryResult + alias PlausibleWeb.Components.Dashboard.ReportList + + import PlausibleWeb.Components.Dashboard.Base + + @pagination %{limit: 50, offset: 0} + + def update(assigns, socket) do + close_url = Utils.dashboard_route(assigns.site, assigns.params) + + socket = + assign(socket, + site: assigns.site, + params: assigns.params, + open?: assigns.open?, + connected?: assigns.connected?, + close_url: close_url + ) + |> load_stats() + + {:ok, socket} + end + + def render(assigns) do + ~H""" +
+ <.modal + id="pages-breakdown-details-modal" + on_close={JS.patch(@close_url)} + show={@open?} + ready={@connected?} + > + <.modal_title> + Top Pages + + +
+ +
+ +
+ """ + end + + defp load_stats(socket) do + %{site: site, params: params} = socket.assigns + + metrics = choose_metrics(params) + + params = + params + |> ParsedQueryParams.set( + metrics: metrics, + dimensions: ["event:page"], + pagination: @pagination + ) + + query = QueryBuilder.build!(site, params) + + %QueryResult{} = query_result = Stats.query(site, query) + + assign(socket, :query_result, query_result) + end + + defp choose_metrics(%ParsedQueryParams{} = params) do + if ParsedQueryParams.conversion_goal_filter?(params) do + [:visitors, :group_conversion_rate] + else + [:visitors] + end + end +end diff --git a/lib/plausible_web/live/dashboard/sources_details.ex b/lib/plausible_web/live/dashboard/sources_details.ex new file mode 100644 index 000000000000..1114aea33a3f --- /dev/null +++ b/lib/plausible_web/live/dashboard/sources_details.ex @@ -0,0 +1,91 @@ +defmodule PlausibleWeb.Live.Dashboard.SourcesDetails do + @moduledoc """ + Sources details modal breakdown component. + """ + + use PlausibleWeb, :live_component + + alias Plausible.Stats + alias Plausible.Stats.Dashboard.Utils + alias Plausible.Stats.ParsedQueryParams + alias Plausible.Stats.QueryBuilder + alias Plausible.Stats.QueryResult + alias PlausibleWeb.Components.Dashboard.ReportList + + import PlausibleWeb.Components.Dashboard.Base + + @pagination %{limit: 50, offset: 0} + + def update(assigns, socket) do + close_url = Utils.dashboard_route(assigns.site, assigns.params) + + socket = + assign(socket, + site: assigns.site, + params: assigns.params, + open?: assigns.open?, + connected?: assigns.connected?, + close_url: close_url + ) + |> load_stats() + + {:ok, socket} + end + + def render(assigns) do + ~H""" +
+ <.modal + id="sources-breakdown-details-modal" + on_close={JS.patch(@close_url)} + show={@open?} + ready={@connected?} + > + <.modal_title> + Sources + + +
+ +
+ +
+ """ + end + + defp load_stats(socket) do + %{site: site, params: params} = socket.assigns + + metrics = choose_metrics(params) + + params = + params + |> ParsedQueryParams.set( + metrics: metrics, + dimensions: ["visit:source"], + pagination: @pagination + ) + + query = QueryBuilder.build!(site, params) + + %QueryResult{} = query_result = Stats.query(site, query) + + assign(socket, :query_result, query_result) + end + + defp choose_metrics(%ParsedQueryParams{} = params) do + if ParsedQueryParams.conversion_goal_filter?(params) do + [:visitors, :group_conversion_rate] + else + [:visitors] + end + end +end diff --git a/lib/plausible_web/live/dashboard/utm_mediums_details.ex b/lib/plausible_web/live/dashboard/utm_mediums_details.ex new file mode 100644 index 000000000000..c1a686c01986 --- /dev/null +++ b/lib/plausible_web/live/dashboard/utm_mediums_details.ex @@ -0,0 +1,91 @@ +defmodule PlausibleWeb.Live.Dashboard.UtmMediumsDetails do + @moduledoc """ + Channels details modal breakdown component. + """ + + use PlausibleWeb, :live_component + + alias Plausible.Stats + alias Plausible.Stats.Dashboard.Utils + alias Plausible.Stats.ParsedQueryParams + alias Plausible.Stats.QueryBuilder + alias Plausible.Stats.QueryResult + alias PlausibleWeb.Components.Dashboard.ReportList + + import PlausibleWeb.Components.Dashboard.Base + + @pagination %{limit: 50, offset: 0} + + def update(assigns, socket) do + close_url = Utils.dashboard_route(assigns.site, assigns.params) + + socket = + assign(socket, + site: assigns.site, + params: assigns.params, + open?: assigns.open?, + connected?: assigns.connected?, + close_url: close_url + ) + |> load_stats() + + {:ok, socket} + end + + def render(assigns) do + ~H""" +
+ <.modal + id="utm-mediums-breakdown-details-modal" + on_close={JS.patch(@close_url)} + show={@open?} + ready={@connected?} + > + <.modal_title> + UTM mediums + + +
+ +
+ +
+ """ + end + + defp load_stats(socket) do + %{site: site, params: params} = socket.assigns + + metrics = choose_metrics(params) + + params = + params + |> ParsedQueryParams.set( + metrics: metrics, + dimensions: ["visit:utm_medium"], + pagination: @pagination + ) + + query = QueryBuilder.build!(site, params) + + %QueryResult{} = query_result = Stats.query(site, query) + + assign(socket, :query_result, query_result) + end + + defp choose_metrics(%ParsedQueryParams{} = params) do + if ParsedQueryParams.conversion_goal_filter?(params) do + [:visitors, :group_conversion_rate] + else + [:visitors] + end + end +end