Skip to content

Commit 05f72e2

Browse files
committed
Implement basics for LV pages breakdown
1 parent f91c217 commit 05f72e2

10 files changed

Lines changed: 269 additions & 13 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react'
2+
3+
type LiveViewPortalProps = {
4+
id: string
5+
className?: string
6+
}
7+
8+
export const LiveViewPortal = React.memo(
9+
function ({ id, className }: LiveViewPortalProps) {
10+
return (
11+
<div
12+
id={id}
13+
className={className}
14+
style={{ width: '100%', border: '0' }}
15+
/>
16+
)
17+
},
18+
() => true
19+
)

assets/js/dashboard/index.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useMemo, useState, useEffect } from 'react'
2+
import { LiveViewPortal } from './components/liveview-portal'
23
import VisitorGraph from './stats/graph/visitor-graph'
34
import Sources from './stats/sources'
45
import Pages from './stats/pages'
@@ -7,6 +8,7 @@ import Devices from './stats/devices'
78
import { TopBar } from './nav-menu/top-bar'
89
import Behaviours from './stats/behaviours'
910
import { useQueryContext } from './query-context'
11+
import { useSiteContext } from './site-context'
1012
import { isRealTimeDashboard } from './util/filters'
1113
import { useAppNavigate } from './navigation/use-app-navigate'
1214
import { parseSearch } from './util/url-search-params'
@@ -19,6 +21,7 @@ function DashboardStats({
1921
updateImportedDataInView?: (v: boolean) => void
2022
}) {
2123
const navigate = useAppNavigate()
24+
const site = useSiteContext()
2225

2326
useEffect(() => {
2427
const unsubscribe = window.addEventListener('dashboard:live-navigate', ((
@@ -44,7 +47,14 @@ function DashboardStats({
4447
<Sources />
4548
</div>
4649
<div className={statsBoxClass}>
47-
<Pages />
50+
{site.flags.live_dashboard ? (
51+
<LiveViewPortal
52+
id="pages-breakdown-live"
53+
className="w-full h-full border-0 overflow-hidden"
54+
/>
55+
) : (
56+
<Pages />
57+
)}
4858
</div>
4959
</div>
5060

assets/js/dashboard/site-context.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ export function parseSiteFromDataset(dataset: DOMStringMap): PlausibleSite {
2727
}
2828

2929
// Update this object when new feature flags are added to the frontend.
30-
type FeatureFlags = Record<never, boolean>
30+
type FeatureFlags = {
31+
live_dashboard?: boolean
32+
}
3133

3234
export const siteContextDefaultValue = {
3335
domain: '',

assets/js/liveview/live_dashboard.js

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,10 @@ const WIDGETS = {
88
initialize: function () {
99
this.url = window.location.href
1010

11-
addListener.bind(this)('phx:update_local_storage', window, (e) => {
12-
localStorage.setItem(e.detail.key, e.detail.value)
13-
})
14-
15-
addListener.bind(this)('phx:update_local_storage', this.el, (e) => {
11+
addListener.bind(this)('click', document.body, (e) => {
1612
const type = e.target.dataset.type || null
1713

18-
if (type && type == 'dashboard-link') {
14+
if (type === 'dashboard-link') {
1915
this.url = e.target.href
2016
const uri = new URL(this.url)
2117
const path = '/' + uri.pathname.split('/').slice(2).join('/')
@@ -54,9 +50,49 @@ const WIDGETS = {
5450
cleanup: function () {
5551
removeListeners.bind(this)()
5652
}
53+
},
54+
tabs: {
55+
initialize: function () {
56+
const domain = getDomain(window.location.href)
57+
58+
addListener.bind(this)('click', this.el, (e) => {
59+
const button = e.target.closest('button')
60+
const tab = button && button.dataset.tab
61+
62+
if (tab) {
63+
const label = button.dataset.label
64+
const storageKey = button.dataset.storageKey
65+
const activeClasses = button.dataset.activeClasses
66+
const inactiveClasses = button.dataset.inactiveClasses
67+
const title = this.el
68+
.closest('[data-tile]')
69+
.querySelector('[data-title]')
70+
71+
title.innerText = label
72+
73+
this.el.querySelectorAll(`button[data-tab] span`).forEach((s) => {
74+
s.className = inactiveClasses
75+
})
76+
77+
button.querySelector('span').className = activeClasses
78+
79+
if (storageKey) {
80+
localStorage.setItem(`${storageKey}__${domain}`, tab)
81+
}
82+
}
83+
})
84+
},
85+
cleanup: function () {
86+
removeListeners.bind(this)()
87+
}
5788
}
5889
}
5990

91+
function getDomain(url) {
92+
const uri = typeof url === 'object' ? url : new URL(url)
93+
return uri.pathname.split('/')[1]
94+
}
95+
6096
function addListener(eventName, listener, callback) {
6197
this.listeners = this.listeners || []
6298

assets/js/liveview/live_socket.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ if (csrfToken && websocketUrl) {
7373
if (domainName) {
7474
return {
7575
user_prefs: {
76-
page_tab: localStorage.getItem(`pageTab__${domainName}`)
76+
pages_tab: localStorage.getItem(`pageTab__${domainName}`)
7777
},
7878
_csrf_token: token
7979
}

lib/plausible_web/controllers/stats_controller.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ defmodule PlausibleWeb.StatsController do
100100
hide_footer?: if(ce?() || demo, do: false, else: site_role != :public),
101101
consolidated_view?: consolidated_view?,
102102
consolidated_view_available?: consolidated_view_available?,
103-
team_identifier: team_identifier
103+
team_identifier: team_identifier,
104+
connect_live_socket: PlausibleWeb.Live.Dashboard.enabled?(site)
104105
)
105106

106107
!stats_start_date && can_see_stats? ->
@@ -447,7 +448,7 @@ defmodule PlausibleWeb.StatsController do
447448

448449
defp get_flags(user, site),
449450
do:
450-
[]
451+
[:live_dashboard]
451452
|> Enum.map(fn flag ->
452453
{flag, FunWithFlags.enabled?(flag, for: user) || FunWithFlags.enabled?(flag, for: site)}
453454
end)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
defmodule PlausibleWeb.Components.Dashboard.Base do
2+
@moduledoc """
3+
Common components for dasbhaord.
4+
"""
5+
6+
use PlausibleWeb, :component
7+
8+
attr :href, :string, required: true
9+
attr :site, Plausible.Site, required: true
10+
attr :class, :string, default: ""
11+
attr :rest, :global
12+
slot :inner_block, required: true
13+
14+
def dashboard_link(assigns) do
15+
url = "/" <> assigns.site.domain <> assigns.href
16+
17+
assigns = assign(assigns, :url, url)
18+
19+
~H"""
20+
<.link
21+
data-type="dashboard-link"
22+
href={@url}
23+
{@rest}
24+
>
25+
{render_slot(@inner_block)}
26+
</.link>
27+
"""
28+
end
29+
end
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
defmodule PlausibleWeb.Components.Dashboard.Tile do
2+
@moduledoc false
3+
4+
use PlausibleWeb, :component
5+
6+
attr :id, :string, required: true
7+
attr :title, :string, required: true
8+
9+
slot :tabs
10+
slot :inner_block, required: true
11+
12+
def tile(assigns) do
13+
~H"""
14+
<div data-tile id={@id}>
15+
<div data-tile class="w-full flex justify-between h-full">
16+
<div class="flex gap-x-1">
17+
<h3 data-title class="font-bold dark:text-gray-100">{@title}</h3>
18+
</div>
19+
20+
<div
21+
:if={@tabs != []}
22+
id={@id <> "-tabs"}
23+
phx-hook="LiveDashboard"
24+
data-widget="tabs"
25+
class="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2 items-baseline"
26+
>
27+
{render_slot(@tabs)}
28+
</div>
29+
</div>
30+
31+
{render_slot(@inner_block)}
32+
</div>
33+
"""
34+
end
35+
36+
attr :id, :string, required: true
37+
slot :inner_block, required: true
38+
39+
def tabs(assigns) do
40+
~H"""
41+
<div
42+
id={@id}
43+
phx-hook="LiveDashboard"
44+
data-widget="tabs"
45+
class="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2 items-baseline"
46+
>
47+
{render_slot(@inner_block)}
48+
</div>
49+
"""
50+
end
51+
52+
attr :label, :string, required: true
53+
attr :value, :string, required: true
54+
attr :active, :string, required: true
55+
attr :target, :any, required: true
56+
57+
def tab(assigns) do
58+
assigns =
59+
assign(assigns,
60+
active_classes:
61+
"text-indigo-600 dark:text-indigo-500 font-bold underline decoration-2 decoration-indigo-600 dark:decoration-indigo-500",
62+
inactive_classes: "hover:text-indigo-700 dark:hover:text-indigo-400 cursor-pointer"
63+
)
64+
65+
~H"""
66+
<button
67+
class="rounded-sm truncate text-left transition-colors duration-150"
68+
data-tab={@value}
69+
data-label={@label}
70+
data-storage-key="pageTab"
71+
data-active-classes={@active_classes}
72+
data-inactive-classes={@inactive_classes}
73+
phx-click="set-tab"
74+
phx-value-tab={@value}
75+
phx-target={@target}
76+
>
77+
<span class={if(@value == @active, do: @active_classes, else: @inactive_classes)}>
78+
{@label}
79+
</span>
80+
</button>
81+
"""
82+
end
83+
end

lib/plausible_web/live/dashboard.ex

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ defmodule PlausibleWeb.Live.Dashboard do
2121
# As domain is passed via session, the associated site has already passed
2222
# validation logic on plug level.
2323
site =
24-
Repo.get_by!(Plausible.Site, domain: domain)
24+
Plausible.Site
25+
|> Repo.get_by!(domain: domain)
2526
|> Repo.preload([
2627
:owners,
2728
:completed_imports,
@@ -45,7 +46,16 @@ defmodule PlausibleWeb.Live.Dashboard do
4546

4647
def render(assigns) do
4748
~H"""
48-
<div id="live-dashboard-container"></div>
49+
<div id="live-dashboard-container" phx-hook="LiveDashboard" data-widget="dashboard-root">
50+
<.portal id="pages-breakdown-live-container" target="#pages-breakdown-live">
51+
<.live_component
52+
module={PlausibleWeb.Live.Dashboard.Pages}
53+
id="pages-breakdown-component"
54+
site={@site}
55+
user_prefs={@user_prefs}
56+
/>
57+
</.portal>
58+
</div>
4959
"""
5060
end
5161

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
defmodule PlausibleWeb.Live.Dashboard.Pages do
2+
@moduledoc """
3+
Pages breakdown component.
4+
"""
5+
6+
use PlausibleWeb, :live_component
7+
8+
alias PlausibleWeb.Components.Dashboard.Base
9+
alias PlausibleWeb.Components.Dashboard.Tile
10+
11+
@tabs [
12+
{"pages", "Top Pages"},
13+
{"entry-pages", "Entry Pages"},
14+
{"exit-pages", "Exit Pages"}
15+
]
16+
17+
@tab_labels Map.new(@tabs)
18+
19+
def update(assigns, socket) do
20+
active_tab = assigns.user_prefs["pages_tab"] || "pages"
21+
22+
socket =
23+
assign(socket,
24+
site: assigns.site,
25+
tabs: @tabs,
26+
tab_labels: @tab_labels,
27+
active_tab: active_tab
28+
)
29+
30+
{:ok, socket}
31+
end
32+
33+
def render(assigns) do
34+
~H"""
35+
<div>
36+
<Tile.tile id="breakdown-tile-pages" title={@tab_labels[@active_tab]}>
37+
<:tabs>
38+
<Tile.tab
39+
:for={{value, label} <- @tabs}
40+
label={label}
41+
value={value}
42+
active={@active_tab}
43+
target={@myself}
44+
/>
45+
</:tabs>
46+
47+
<div class="mx-auto font-medium text-gray-500 dark:text-gray-400">
48+
<Base.dashboard_link site={@site} href="?f=is,source,Direct / None">
49+
Filter by source Direct / None
50+
</Base.dashboard_link>
51+
</div>
52+
</Tile.tile>
53+
</div>
54+
"""
55+
end
56+
57+
def handle_event("set-tab", %{"tab" => tab}, socket) do
58+
if tab != socket.assigns.active_tab do
59+
socket = assign(socket, :active_tab, tab)
60+
61+
{:noreply, socket}
62+
else
63+
{:noreply, socket}
64+
end
65+
end
66+
end

0 commit comments

Comments
 (0)