Skip to content

Commit 997546f

Browse files
authored
Another iteration of Dashboard.Live.Pages (#5986)
* move DashboardQueryParser and serializer to subfolder * add external link construction logic * default include.imports_meta to false This will only be necessary for Top Stats, which will be responsible for rendering the "with_imported_switch". Defaulting to `true` would mean `imports_skip_reason` always being returned under `meta` even when imported data was not requested. * imported warning with tests (no tooltip yet) * switch to new styling * query the right conversion rate * fix external link display on hover * replicate initial loading state in React * add missing class * remove TODO comments * fix tests * fix ce_test * rename modules * use storage util
1 parent 8795a3e commit 997546f

File tree

19 files changed

+387
-191
lines changed

19 files changed

+387
-191
lines changed

assets/js/dashboard/components/liveview-portal.tsx

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,58 @@
77

88
import React from 'react'
99
import classNames from 'classnames'
10+
import { ReportHeader } from '../stats/reports/report-header'
11+
import { ReportLayout } from '../stats/reports/report-layout'
12+
import { TabButton, TabWrapper } from './tabs'
13+
import { MoreLinkState } from '../stats/more-link-state'
14+
import MoreLink from '../stats/more-link'
1015

11-
const MIN_HEIGHT = 380
16+
const MIN_HEIGHT = 356
1217

1318
type LiveViewPortalProps = {
1419
id: string
20+
tabs: { value: string; label: string }[]
21+
storageKey: string
1522
className?: string
1623
}
1724

1825
export const LiveViewPortal = React.memo(
19-
function ({ id, className }: LiveViewPortalProps) {
26+
function ({ id, tabs, storageKey, className }: LiveViewPortalProps) {
27+
const activeTab = localStorage.getItem(storageKey) || 'pages'
28+
2029
return (
2130
<div
2231
id={id}
2332
className={classNames('group', className)}
2433
style={{ width: '100%', border: '0', minHeight: MIN_HEIGHT }}
2534
>
26-
<div
27-
className="w-full flex flex-col justify-center group-has-[[data-phx-teleported]]:hidden"
28-
style={{ minHeight: MIN_HEIGHT }}
29-
>
30-
<div className="mx-auto loading">
31-
<div />
32-
</div>
35+
<div className={'group-has-[[data-phx-teleported]]:hidden'}>
36+
<ReportLayout>
37+
<ReportHeader>
38+
<div className="flex gap-x-3">
39+
<TabWrapper>
40+
{tabs.map(({ value, label }) => (
41+
<TabButton
42+
key={value}
43+
active={activeTab === value}
44+
onClick={() => {}}
45+
>
46+
{label}
47+
</TabButton>
48+
))}
49+
</TabWrapper>
50+
</div>
51+
<MoreLink state={MoreLinkState.LOADING} linkProps={undefined} />
52+
</ReportHeader>
53+
<div
54+
className="w-full flex flex-col justify-center"
55+
style={{ minHeight: `${MIN_HEIGHT}px` }}
56+
>
57+
<div className="mx-auto loading">
58+
<div></div>
59+
</div>
60+
</div>
61+
</ReportLayout>
3362
</div>
3463
</div>
3564
)

assets/js/dashboard/index.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import { TopBar } from './nav-menu/top-bar'
99
import Behaviours from './stats/behaviours'
1010
import { useQueryContext } from './query-context'
1111
import { useSiteContext } from './site-context'
12-
import { isRealTimeDashboard } from './util/filters'
12+
import { hasConversionGoalFilter, isRealTimeDashboard } from './util/filters'
1313
import { useAppNavigate } from './navigation/use-app-navigate'
1414
import { parseSearch } from './util/url-search-params'
15+
import { getDomainScopedStorageKey } from './util/storage'
1516

1617
function DashboardStats({
1718
importedDataInView,
@@ -22,6 +23,7 @@ function DashboardStats({
2223
}) {
2324
const navigate = useAppNavigate()
2425
const site = useSiteContext()
26+
const { query } = useQueryContext()
2527

2628
// Handler for navigation events delegated from LiveView dashboard.
2729
// Necessary to emulate navigation events in LiveView with pushState
@@ -57,6 +59,17 @@ function DashboardStats({
5759
{site.flags.live_dashboard ? (
5860
<LiveViewPortal
5961
id="pages-breakdown-live"
62+
tabs={[
63+
{
64+
label: hasConversionGoalFilter(query)
65+
? 'Conversion pages'
66+
: 'Top pages',
67+
value: 'pages'
68+
},
69+
{ label: 'Entry pages', value: 'entry-pages' },
70+
{ label: 'Exit pages', value: 'exit-pages' }
71+
]}
72+
storageKey={getDomainScopedStorageKey('pageTab', site.domain)}
6073
className="w-full h-full border-0 overflow-hidden"
6174
/>
6275
) : (

assets/js/liveview/dashboard_tabs.js

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,29 @@ export default buildHook({
1717
this.addListener('click', this.el, (e) => {
1818
const button = e.target.closest('button')
1919
const tabKey = button && button.dataset.tabKey
20-
const span = button && button.querySelector('span')
2120

22-
if (span && span.dataset.active === 'false') {
23-
const reportLabel = button.dataset.reportLabel
21+
if (button && button.closest('div').dataset.active === 'false') {
2422
const storageKey = button.dataset.storageKey
2523
const target = button.dataset.target
26-
const tile = this.el.closest('[data-tile]')
27-
const title = tile.querySelector('[data-title]')
2824

29-
title.innerText = reportLabel
30-
31-
this.el.querySelectorAll(`button[data-tab-key] span`).forEach((s) => {
32-
this.js().setAttribute(s, 'data-active', 'false')
25+
this.el.querySelectorAll(`button[data-tab-key]`).forEach((b) => {
26+
if (b.dataset.tabKey === tabKey) {
27+
this.js().setAttribute(b.closest('div'), 'data-active', 'true')
28+
this.js().setAttribute(
29+
b.querySelector('span'),
30+
'data-active',
31+
'true'
32+
)
33+
} else {
34+
this.js().setAttribute(b.closest('div'), 'data-active', 'false')
35+
this.js().setAttribute(
36+
b.querySelector('span'),
37+
'data-active',
38+
'false'
39+
)
40+
}
3341
})
3442

35-
this.js().setAttribute(
36-
button.querySelector('span'),
37-
'data-active',
38-
'true'
39-
)
40-
4143
if (storageKey) {
4244
localStorage.setItem(`${storageKey}__${domain}`, tabKey)
4345
}

lib/plausible/stats/dashboard_query_parser.ex renamed to lib/plausible/stats/dashboard/query_parser.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
defmodule Plausible.Stats.DashboardQueryParser do
1+
defmodule Plausible.Stats.Dashboard.QueryParser do
22
@moduledoc """
33
Parses a dashboard query string into `%ParsedQueryParams{}`. Note that
44
`metrics` and `dimensions` do not exist at this step yet, and are expected
@@ -13,7 +13,7 @@ defmodule Plausible.Stats.DashboardQueryParser do
1313
# is false. Even if we don't want to include imported data, we
1414
# might still want to know whether imported data can be toggled
1515
# on/off on the dashboard.
16-
imports_meta: true,
16+
imports_meta: false,
1717
time_labels: false,
1818
total_rows: false,
1919
trim_relative_date_range: true,

lib/plausible/stats/dashboard_query_serializer.ex renamed to lib/plausible/stats/dashboard/query_serializer.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
defmodule Plausible.Stats.DashboardQuerySerializer do
1+
defmodule Plausible.Stats.Dashboard.QuerySerializer do
22
@moduledoc """
33
Takes a `%ParsedQueryParams{}` struct and turns it into a query
44
string.
55
"""
66

7-
alias Plausible.Stats.{ParsedQueryParams, DashboardQueryParser, QueryInclude}
7+
alias Plausible.Stats.{ParsedQueryParams, Dashboard, QueryInclude}
88

99
def serialize(%ParsedQueryParams{} = params, segments \\ []) do
1010
params
@@ -62,7 +62,7 @@ defmodule Plausible.Stats.DashboardQuerySerializer do
6262
defp get_serialized_fields(_, _), do: []
6363

6464
defp get_serialized_fields_from_include(:imports, %QueryInclude{} = include) do
65-
if include.imports == DashboardQueryParser.default_include().imports do
65+
if include.imports == Dashboard.QueryParser.default_include().imports do
6666
[]
6767
else
6868
[{"with_imported", to_string(include.imports)}]
@@ -88,7 +88,7 @@ defmodule Plausible.Stats.DashboardQuerySerializer do
8888

8989
defp get_serialized_fields_from_include(:compare_match_day_of_week, include) do
9090
if include.compare_match_day_of_week ==
91-
DashboardQueryParser.default_include().compare_match_day_of_week do
91+
Dashboard.QueryParser.default_include().compare_match_day_of_week do
9292
[]
9393
else
9494
[{"match_day_of_week", to_string(include.compare_match_day_of_week)}]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
defmodule Plausible.Stats.Dashboard.Utils do
2+
@moduledoc """
3+
Shared utilities by different dashboard reports.
4+
"""
5+
6+
alias Plausible.Site
7+
alias Plausible.Stats.{Dashboard, ParsedQueryParams}
8+
9+
def page_external_link_fn_for(site) do
10+
with true <- Plausible.Sites.regular?(site),
11+
[domain | _] <- String.split(site.domain, "/"),
12+
{:ok, domain} <- idna_encode(domain),
13+
{:ok, uri} <- URI.new("https://#{domain}/") do
14+
fn item ->
15+
"https://#{uri.host}#{hd(item.dimensions)}"
16+
end
17+
else
18+
_ -> nil
19+
end
20+
end
21+
22+
def dashboard_route(%Site{} = site, %ParsedQueryParams{} = params, opts) do
23+
path = Keyword.get(opts, :path, "")
24+
25+
params =
26+
case Keyword.get(opts, :filter) do
27+
nil -> params
28+
filter -> ParsedQueryParams.add_or_replace_filter(params, filter)
29+
end
30+
31+
query_string =
32+
case Dashboard.QuerySerializer.serialize(params) do
33+
"" -> ""
34+
query_string -> "?" <> query_string
35+
end
36+
37+
"/" <> site.domain <> path <> query_string
38+
end
39+
40+
defp idna_encode(domain) do
41+
try do
42+
{:ok, domain |> String.to_charlist() |> :idna.encode() |> IO.iodata_to_binary()}
43+
catch
44+
_ -> {:error, :invalid_domain}
45+
end
46+
end
47+
end

lib/plausible/stats/metrics.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ defmodule Plausible.Stats.Metrics do
6666
def dashboard_metric_label(:visitors, _context), do: "Visitors"
6767

6868
def dashboard_metric_label(:conversion_rate, _context), do: "CR"
69+
def dashboard_metric_label(:group_conversion_rate, _context), do: "CR"
6970

7071
def dashboard_metric_label(metric, _context), do: "#{metric}"
7172
end

lib/plausible_web/live/components/dashboard/base.ex

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,17 @@ defmodule PlausibleWeb.Components.Dashboard.Base do
55

66
use PlausibleWeb, :component
77

8-
alias Plausible.Stats.DashboardQuerySerializer
9-
alias Plausible.Stats.ParsedQueryParams
10-
11-
attr :site, Plausible.Site, required: true
12-
attr :params, :map, required: true
13-
attr :path, :string, default: ""
8+
attr :to, :string, required: true
149
attr :class, :string, default: ""
1510
attr :rest, :global
1611

1712
slot :inner_block, required: true
1813

1914
def dashboard_link(assigns) do
20-
query_string = DashboardQuerySerializer.serialize(assigns.params)
21-
url = "/" <> assigns.site.domain <> assigns.path
22-
23-
url =
24-
if query_string != "" do
25-
url <> "?" <> query_string
26-
else
27-
url
28-
end
29-
30-
assigns = assign(assigns, :url, url)
31-
3215
~H"""
3316
<.link
3417
data-type="dashboard-link"
35-
patch={@url}
18+
patch={@to}
3619
class={@class}
3720
{@rest}
3821
>
@@ -41,26 +24,6 @@ defmodule PlausibleWeb.Components.Dashboard.Base do
4124
"""
4225
end
4326

44-
attr :site, Plausible.Site, required: true
45-
attr :params, :map, required: true
46-
attr :filter, :list, required: true
47-
attr :class, :string, default: ""
48-
attr :rest, :global
49-
50-
slot :inner_block, required: true
51-
52-
def filter_link(assigns) do
53-
params = ParsedQueryParams.add_or_replace_filter(assigns.params, assigns.filter)
54-
55-
assigns = assign(assigns, :params, params)
56-
57-
~H"""
58-
<.dashboard_link site={@site} params={@params} class={@class} {@rest}>
59-
{render_slot(@inner_block)}
60-
</.dashboard_link>
61-
"""
62-
end
63-
6427
attr :style, :string, default: ""
6528
attr :background_class, :string, default: ""
6629
attr :width, :integer, required: true
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
defmodule PlausibleWeb.Components.Dashboard.ImportedDataWarnings do
2+
@moduledoc false
3+
4+
use PlausibleWeb, :component
5+
alias Phoenix.LiveView.AsyncResult
6+
alias Plausible.Stats.QueryResult
7+
8+
def unsupported_filters(assigns) do
9+
show? =
10+
case assigns.query_result do
11+
%AsyncResult{result: %QueryResult{meta: meta}} ->
12+
meta[:imports_skip_reason] == :unsupported_query
13+
14+
_ ->
15+
false
16+
end
17+
18+
assigns = assign(assigns, :show?, show?)
19+
20+
~H"""
21+
<div :if={@show?} data-test-id="unsupported-filters-warning">
22+
<span class="hidden">
23+
Imported data is excluded due to the applied filters
24+
</span>
25+
<Heroicons.exclamation_circle class="mb-1 size-4.5 text-gray-500 dark:text-gray-400" />
26+
</div>
27+
"""
28+
end
29+
end

lib/plausible_web/live/components/dashboard/metric.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ defmodule PlausibleWeb.Components.Dashboard.Metric do
77

88
@formatters %{
99
visitors: :number_short,
10-
conversion_rate: :percentage
10+
conversion_rate: :percentage,
11+
group_conversion_rate: :percentage
1112
}
1213

1314
attr :name, :atom, required: true

0 commit comments

Comments
 (0)