Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions assets/js/dashboard/components/liveview-portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'

type LiveViewPortalProps = {
id: string
className?: string
}

export const LiveViewPortal = React.memo(
function ({ id, className }: LiveViewPortalProps) {
return (
<div
id={id}
className={className}
style={{ width: '100%', border: '0' }}
/>
)
},
() => true
)
26 changes: 23 additions & 3 deletions assets/js/dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React, { useMemo, useState } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import VisitorGraph from './stats/graph/visitor-graph'
import Sources from './stats/sources'
import Pages from './stats/pages'
import { LiveViewPortal } from './components/liveview-portal'
import Locations from './stats/locations'
import Devices from './stats/devices'
import { TopBar } from './nav-menu/top-bar'
import Behaviours from './stats/behaviours'
import { useQueryContext } from './query-context'
import { isRealTimeDashboard } from './util/filters'
import { useAppNavigate } from './navigation/use-app-navigate'
import { parseSearch } from './util/url-search-params'

function DashboardStats({
importedDataInView,
Expand All @@ -16,6 +18,21 @@ function DashboardStats({
importedDataInView?: boolean
updateImportedDataInView?: (v: boolean) => void
}) {
const navigate = useAppNavigate()

useEffect(() => {
const unsubscribe = window.addEventListener('live-navigate', ((
e: CustomEvent
) => {
navigate({
path: e.detail.path,
search: () => parseSearch(e.detail.search),
replace: true
})
}) as EventListener)
return unsubscribe
}, [navigate])

const statsBoxClass =
'relative min-h-[436px] w-full mt-5 p-4 flex flex-col bg-white dark:bg-gray-900 shadow-sm rounded-md md:min-h-initial md:h-27.25rem md:w-[calc(50%-10px)] md:ml-[10px] md:mr-[10px] first:ml-0 last:mr-0'

Expand All @@ -27,7 +44,10 @@ function DashboardStats({
<Sources />
</div>
<div className={statsBoxClass}>
<Pages />
<LiveViewPortal
id="pages-breakdown-live"
className="w-full h-full border-0 overflow-hidden"
/>
</div>
</div>

Expand Down
6 changes: 6 additions & 0 deletions assets/js/dashboard/navigation/use-app-navigate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ export const useAppNavigate = () => {
search,
...options
}: AppNavigationTarget & NavigateOptions) => {
window.dispatchEvent(
new CustomEvent('live-navigate-back', {
detail: { search: window.location.search }
})
)

return _navigate(getToOptions({ path, params, search }), options)
},
[getToOptions, _navigate]
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export function createAppRouter(site: PlausibleSite) {
utmTermsRoute,
referrersGoogleRoute,
referrersDrilldownRoute,
topPagesRoute,
// topPagesRoute,
entryPagesRoute,
exitPagesRoute,
countriesRoute,
Expand Down
139 changes: 139 additions & 0 deletions assets/js/liveview/live_dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
const WIDGETS = {
'breakdown-tile': {
initialize: function () {
this.url = window.location.href

this.listeners = []

const localStorageListener = (e) => {
localStorage.setItem(e.detail.key, e.detail.value)
}

window.addEventListener('phx:update_local_storage', localStorageListener)

this.listeners.push({
element: window,
event: 'phx:update_local_storage',
callback: localStorageListener
})

const closeModalListener = ((e) => {

Check failure on line 20 in assets/js/liveview/live_dashboard.js

View workflow job for this annotation

GitHub Actions / Build and test

'e' is defined but never used. Allowed unused args must match /^_/u
this.el.dispatchEvent(
new CustomEvent('live-navigate', {
bubbles: true,
detail: { path: '/', search: window.location.search }
})
)
}).bind(this)

window.addEventListener('dashboard:close_modal', closeModalListener)

this.listeners.push({
element: window,
event: 'dashboard:close_modal',
callback: closeModalListener
})

const clickListener = ((e) => {
const type = e.target.dataset.type || null

if (type && type == 'dashboard-link') {
this.url = e.target.href
const uri = new URL(this.url)
const path = '/' + uri.pathname.split('/').slice(2).join('/')
this.el.dispatchEvent(
new CustomEvent('live-navigate', {
bubbles: true,
detail: { path: path, search: uri.search }
})
)

this.pushEvent('handle_dashboard_params', { url: this.url })

e.preventDefault()
}
}).bind(this)

this.el.addEventListener('click', clickListener)

this.listeners.push({
element: this.el,
event: 'click',
callback: clickListener
})

const popListener = (() => {
if (this.url !== window.location.href) {
this.pushEvent('handle_dashboard_params', {
url: window.location.href
})
}
}).bind(this)

window.addEventListener('popstate', popListener)

this.listeners.push({
element: window,
event: 'popstate',
callback: popListener
})

const backListener = ((e) => {
if (
typeof e.detail.search === 'string' &&
this.url !== window.location.href
) {
this.pushEvent('handle_dashboard_params', {
url: window.location.href
})
}
}).bind(this)

window.addEventListener('live-navigate-back', backListener)

this.listeners.push({
element: window,
event: 'live-navigate-back',
callback: backListener
})
},
cleanup: function () {
if (this.listeners) {
this.listeners.forEach((l) => {
l.element.removeEventListener(l.event, l.callback)
})

this.listeners = null
}
}
}
}

export default {
mounted() {
this.widget = this.el.getAttribute('data-widget')

this.initialize()
},

updated() {
this.initialize()
},

reconnected() {
this.initialize()
},

destroyed() {
this.cleanup()
},

initialize() {
this.cleanup()
WIDGETS[this.widget].initialize.bind(this)()
},

cleanup() {
WIDGETS[this.widget].cleanup.bind(this)()
}
}
25 changes: 23 additions & 2 deletions assets/js/liveview/live_socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ import 'phoenix_html'
import { Socket } from 'phoenix'
import { LiveSocket } from 'phoenix_live_view'
import { Modal, Dropdown } from 'prima'
import LiveDashboard from './live_dashboard'
import topbar from 'topbar'
/* eslint-enable import/no-unresolved */

import Alpine from 'alpinejs'

let csrfToken = document.querySelector("meta[name='csrf-token']")
let websocketUrl = document.querySelector("meta[name='websocket-url']")
let disablePushStateFlag = document.querySelector(
"meta[name='live-socket-disable-push-state']"
)
let domain = document.querySelector("meta[name='dashboard-domain']")
if (csrfToken && websocketUrl) {
let Hooks = { Modal, Dropdown }
let Hooks = { Modal, Dropdown, LiveDashboard }
Hooks.Metrics = {
mounted() {
this.handleEvent('send-metrics', ({ event_name }) => {
Expand Down Expand Up @@ -48,9 +53,13 @@ if (csrfToken && websocketUrl) {
let token = csrfToken.getAttribute('content')
let url = websocketUrl.getAttribute('content')
let liveUrl = url === '' ? '/live' : new URL('/live', url).href
let disablePushState =
!!disablePushStateFlag &&
disablePushStateFlag.getAttribute('content') === 'true'
let domainName = domain && domain.getAttribute('content')
let liveSocket = new LiveSocket(liveUrl, Socket, {
disablePushState: disablePushState,
heartbeatIntervalMs: 10000,
params: { _csrf_token: token },
hooks: Hooks,
uploaders: Uploaders,
dom: {
Expand All @@ -60,6 +69,18 @@ if (csrfToken && websocketUrl) {
Alpine.clone(from, to)
}
}
},
params: () => {
if (domainName) {
return {
user_prefs: {
page_tab: localStorage.getItem(`pageTab__${domainName}`)
},
_csrf_token: token
}
} else {
return { _csrf_token: token }
}
}
})

Expand Down
17 changes: 17 additions & 0 deletions lib/plausible_web/components/generic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,23 @@ defmodule PlausibleWeb.Components.Generic do
end
end

attr(:href, :string, required: true)
attr(:class, :string, default: "")
attr(:rest, :global)
slot(:inner_block, required: true)

def dashboard_link(assigns) do
~H"""
<.link
data-type="dashboard-link"
href={@href}
{@rest}
>
{render_slot(@inner_block)}
</.link>
"""
end

attr(:class, :any, default: "")
attr(:rest, :global)

Expand Down
6 changes: 4 additions & 2 deletions lib/plausible_web/controllers/stats_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ defmodule PlausibleWeb.StatsController do

plug(PlausibleWeb.Plugs.AuthorizeSiteAccess when action in [:stats, :csv_export])

def stats(%{assigns: %{site: site}} = conn, _params) do
def stats(%{assigns: %{site: site}} = conn, params) do
site = Plausible.Repo.preload(site, :owners)
site_role = conn.assigns[:site_role]
current_user = conn.assigns[:current_user]
Expand Down Expand Up @@ -100,7 +100,9 @@ defmodule PlausibleWeb.StatsController do
hide_footer?: if(ce?() || demo, do: false, else: site_role != :public),
consolidated_view?: consolidated_view?,
consolidated_view_available?: consolidated_view_available?,
team_identifier: team_identifier
team_identifier: team_identifier,
connect_live_socket: true,
params: params
)

!stats_start_date && can_see_stats? ->
Expand Down
3 changes: 2 additions & 1 deletion lib/plausible_web/live/components/prima_modal.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ defmodule PlausibleWeb.Live.Components.PrimaModal do
attr :id, :string, required: true
attr :use_portal?, :boolean, default: Mix.env() not in [:test, :ce_test]
slot :inner_block, required: true
attr :on_close, JS, default: %JS{}

def modal(assigns) do
~H"""
<Modal.modal portal={@use_portal?} id={@id}>
<Modal.modal portal={@use_portal?} id={@id} on_close={@on_close}>
<Modal.modal_overlay
transition_enter={{"ease-out duration-300", "opacity-0", "opacity-100"}}
transition_leave={{"ease-in duration-200", "opacity-100", "opacity-0"}}
Expand Down
Loading
Loading