Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5743b4e
render dashboard directly from Live.Dashboard
RobertJoonas Jan 8, 2026
4580cbb
LV from router, ignore ff
RobertJoonas Jan 8, 2026
65c7b28
add sources tile
RobertJoonas Jan 9, 2026
ca4f4f2
move dashboard periods to separate context
RobertJoonas Jan 12, 2026
45d9b4a
fix sources after rebase
RobertJoonas Jan 12, 2026
e240554
WIP integrate prima fork (broken)
RobertJoonas Jan 12, 2026
49142e4
fix sha in mix lock
RobertJoonas Jan 13, 2026
13f5eed
Liveview dashboard datepicker (#6000)
RobertJoonas Jan 14, 2026
13c5fa0
Merge branch 'master' into liveview-dashboard
RobertJoonas Jan 15, 2026
22b978c
Add dashboard loading states (#6004)
sanne-san Jan 19, 2026
7ed9d62
Add no data loading state to reports
sanne-san Jan 19, 2026
4661485
render dashboard directly from Live.Dashboard
RobertJoonas Jan 8, 2026
0ce2dc8
LV from router, ignore ff
RobertJoonas Jan 8, 2026
2894f59
add sources tile
RobertJoonas Jan 9, 2026
e6be04c
move dashboard periods to separate context
RobertJoonas Jan 12, 2026
9f2c8dd
fix sources after rebase
RobertJoonas Jan 12, 2026
d5c2edb
WIP integrate prima fork (broken)
RobertJoonas Jan 12, 2026
dc74486
fix sha in mix lock
RobertJoonas Jan 13, 2026
9e883d7
Liveview dashboard datepicker (#6000)
RobertJoonas Jan 14, 2026
e7c0ba4
Add dashboard loading states (#6004)
sanne-san Jan 19, 2026
8b9eade
Liveview dashboard details modals (#6002)
zoldar Jan 20, 2026
565281e
Merge branch 'liveview-dashboard' into sanne-nodata-loading
sanne-san Jan 20, 2026
4bf3256
Add no data loading state to reports
sanne-san Jan 20, 2026
0b244c5
Prevent "freeze" of modal during close and open and fix history handl…
zoldar Jan 22, 2026
c33140a
Refresh stats every 30s in realtime mode (#6012)
RobertJoonas Jan 22, 2026
8a6c6fa
Disable LV progress bar at the top of the window for dashboard
zoldar Jan 22, 2026
c298309
Merge branch 'master' into liveview-dashboard
RobertJoonas Jan 26, 2026
eab2ed4
Liveview dashboard keyboard shortcuts (#6020)
RobertJoonas Jan 26, 2026
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
16 changes: 16 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@
--default-ring-color: var(--color-indigo-500);
}

@layer utilities {
.animate-pulse {
animation: pulse 1.25s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
}

[class*='group/report']:has(.tile-tabs.phx-hook-loading)
[class*='group-has-[.tile-tabs.phx-hook-loading]/report:animate-pulse'] {
animation: pulse 1.25s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
}

[class*='group/dashboard'].phx-navigation-loading
[class*='group-[.phx-navigation-loading]/dashboard:animate-pulse'] {
animation: pulse 1.25s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
}
}

@media print {
canvas {
width: 100% !important;
Expand Down
28 changes: 2 additions & 26 deletions assets/js/dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react'
import { LiveViewPortal } from './components/liveview-portal'
import VisitorGraph from './stats/graph/visitor-graph'
import Sources from './stats/sources'
import Pages from './stats/pages'
Expand All @@ -8,11 +7,9 @@ import Devices from './stats/devices'
import { TopBar } from './nav-menu/top-bar'
import Behaviours from './stats/behaviours'
import { useQueryContext } from './query-context'
import { useSiteContext } from './site-context'
import { hasConversionGoalFilter, isRealTimeDashboard } from './util/filters'
import { isRealTimeDashboard } from './util/filters'
import { useAppNavigate } from './navigation/use-app-navigate'
import { parseSearch } from './util/url-search-params'
import { getDomainScopedStorageKey } from './util/storage'

function DashboardStats({
importedDataInView,
Expand All @@ -22,8 +19,6 @@ function DashboardStats({
updateImportedDataInView?: (v: boolean) => void
}) {
const navigate = useAppNavigate()
const site = useSiteContext()
const { query } = useQueryContext()

// Handler for navigation events delegated from LiveView dashboard.
// Necessary to emulate navigation events in LiveView with pushState
Expand Down Expand Up @@ -56,26 +51,7 @@ function DashboardStats({
<>
<VisitorGraph updateImportedDataInView={updateImportedDataInView} />
<Sources />
{site.flags.live_dashboard ? (
<LiveViewPortal
id="pages-breakdown-live"
tabs={[
{
label: hasConversionGoalFilter(query)
? 'Conversion pages'
: 'Top pages',
value: 'pages'
},
{ label: 'Entry pages', value: 'entry-pages' },
{ label: 'Exit pages', value: 'exit-pages' }
]}
storageKey={getDomainScopedStorageKey('pageTab', site.domain)}
className="w-full h-full border-0 overflow-hidden"
/>
) : (
<Pages />
)}

<Pages />
<Locations />
<Devices />
<Behaviours importedDataInView={importedDataInView} />
Expand Down
89 changes: 54 additions & 35 deletions assets/js/liveview/dashboard_root.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,35 @@

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(uri) {
// Domain is dropped from URL prefix, because that's what react-dom-router
// expects.
const path = '/' + uri.pathname.split('/').slice(2).join('/')

const modalId = MODAL_ROUTES[path]

if (modalId) {
const modal = document.querySelector(modalId)

if (modal) {
modal.dispatchEvent(new Event('prima:modal:open'))
}
}
}

const KEYBOARD_SHORTCUTS = {
changePeriod: ['r', 'd', 'm', 'y', 'a', 'w', 'f', 't', 'n', 's', 'l'],
shiftPeriod: ['ArrowLeft', 'ArrowRight'],
clearFilters: 'Escape'
}

export default buildHook({
Expand All @@ -25,40 +43,41 @@ export default buildHook({
this.portalTargets = Array.from(portals, (p) => p.dataset.phxPortal)
this.url = window.location.href

this.addListener('click', document.body, (e) => {
const type = e.target.dataset.type || null
this.addListener('keydown', window, (e) => {
if (KEYBOARD_SHORTCUTS.changePeriod.includes(e.key)) {
window.dispatchEvent(
new CustomEvent('keyboard-change-period', { detail: { key: e.key } })
)
}

if (type === 'dashboard-link') {
const uri = new URL(e.target.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 }
})
if (KEYBOARD_SHORTCUTS.shiftPeriod.includes(e.key)) {
window.dispatchEvent(
new CustomEvent('keyboard-shift-period', { detail: { key: e.key } })
)
}

e.preventDefault()
if (KEYBOARD_SHORTCUTS.clearFilters === e.key) {
this.pushEventTo(this.el, 'clear_filters')
}
})

// Browser back and forward navigation triggers that event.
this.addListener('popstate', window, () => {
if (this.url !== window.location.href) {
navigateWithLoader.bind(this)(window.location.href)
this.addListener('phx:navigate', window, (info) => {
if (info.detail?.patch && info.detail?.pop) {
const uri = new URL(
(info.detail.href.startsWith('http') ? '' : 'https://example.com') +
info.detail.href
)
routeModal(uri)
}
})

// 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)
this.addListener('click', document.body, (e) => {
const link = e.target.closest('[data-phx-link]')
const type = link && (link.dataset.type || null)

if (type === 'dashboard-link') {
const uri = new URL(link.href)
routeModal(uri)
}
})
}
Expand Down
100 changes: 100 additions & 0 deletions assets/js/liveview/datepicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Hook widget for optimistic updates to the datepicker
* label when relative date is changed (prev/next period
* arrow keys).
*/

import { buildHook } from './hook_builder'

function prevPeriod() {
if (this.currentIndex === 0) {
return false
} else {
this.currentIndex--
return true
}
}

function nextPeriod() {
if (this.currentIndex === this.dates.length - 1) {
return false
} else {
this.currentIndex++
return true
}
}

function debounce(fn, delay) {
let timer

return function (...args) {
clearTimeout(timer)

timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}

export default buildHook({
initialize() {
this.currentIndex = parseInt(this.el.dataset.currentIndex)
this.dates = JSON.parse(this.el.dataset.dates)
this.labels = JSON.parse(this.el.dataset.labels)

this.prevPeriodButton = this.el.querySelector('button#prev-period')
this.nextPeriodButton = this.el.querySelector('button#next-period')
this.periodLabel = this.el.querySelector('#period-label')

this.debouncedPushEvent = debounce(() => {
this.pushEventTo(this.el.dataset.target, 'set-relative-date', {
date: this.dates[this.currentIndex]
})
}, 500)

this.handlePeriodShift = (shiftFn) => {
if (this.dates.length) {
const updated = shiftFn.bind(this)()

if (updated) {
this.debouncedPushEvent()
}

this.periodLabel.innerText = this.labels[this.currentIndex]
this.prevPeriodButton.dataset.disabled = `${this.currentIndex == 0}`
this.nextPeriodButton.dataset.disabled = `${this.currentIndex == this.dates.length - 1}`
}
}

this.addListener('keyboard-change-period', window, (e) => {
const periodLink = this.el.querySelector(
`a[data-keyboard-shortcut="${e.detail.key}"]`
)
if (periodLink) {
periodLink.click()
}
})

this.addListener('keyboard-shift-period', window, (e) => {
if (e.detail.key === 'ArrowLeft') {
this.handlePeriodShift(prevPeriod)
}

if (e.detail.key === 'ArrowRight') {
this.handlePeriodShift(nextPeriod)
}
})

this.addListener('click', this.el, (e) => {
const button = e.target.closest('button')

if (button === this.prevPeriodButton) {
this.handlePeriodShift(prevPeriod)
}

if (button === this.nextPeriodButton) {
this.handlePeriodShift(nextPeriod)
}
})
}
})
41 changes: 33 additions & 8 deletions assets/js/liveview/live_socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { LiveSocket } from 'phoenix_live_view'
import { Modal, Dropdown } from 'prima'
import DashboardRoot from './dashboard_root'
import DashboardTabs from './dashboard_tabs.js'
import DatePicker from './datepicker'
import topbar from 'topbar'
/* eslint-enable import/no-unresolved */

Expand All @@ -22,7 +23,7 @@ let disablePushStateFlag = document.querySelector(
)
let domain = document.querySelector("meta[name='dashboard-domain']")
if (csrfToken && websocketUrl) {
let Hooks = { Modal, Dropdown, DashboardRoot, DashboardTabs }
let Hooks = { Modal, Dropdown, DashboardRoot, DashboardTabs, DatePicker }
Hooks.Metrics = {
mounted() {
this.handleEvent('send-metrics', ({ event_name }) => {
Expand Down Expand Up @@ -80,6 +81,7 @@ if (csrfToken && websocketUrl) {
// user preferences across the reloads.
user_prefs: {
pages_tab: localStorage.getItem(`pageTab__${domainName}`),
sources_tab: localStorage.getItem(`sourceTab__${domainName}`),
period: localStorage.getItem(`period__${domainName}`),
comparison: localStorage.getItem(`comparison_mode__${domainName}`),
match_day_of_week: localStorage.getItem(
Expand All @@ -94,13 +96,36 @@ if (csrfToken && websocketUrl) {
}
})

topbar.config({
barColors: { 0: '#303f9f' },
shadowColor: 'rgba(0, 0, 0, .3)',
barThickness: 4
})
window.addEventListener('phx:page-loading-start', (_info) => topbar.show())
window.addEventListener('phx:page-loading-stop', (_info) => topbar.hide())
const dashboardContainer = document.getElementById('live-dashboard-container')

if (!dashboardContainer) {
topbar.config({
barColors: { 0: '#303f9f' },
shadowColor: 'rgba(0, 0, 0, .3)',
barThickness: 4
})

window.addEventListener('phx:page-loading-start', (_info) => topbar.show())
window.addEventListener('phx:page-loading-stop', (_info) => topbar.hide())
}

if (dashboardContainer) {
window.addEventListener('phx:page-loading-start', (info) => {
if (info.detail?.kind === 'patch') {
const uri = new URL(
(info.detail.to.startsWith('http') ? '' : 'https://example.com') +
info.detail.to
)

if (uri.pathname.split('/').length == 2) {
dashboardContainer.classList.add('phx-navigation-loading')
}
}
})
window.addEventListener('phx:page-loading-stop', () => {
dashboardContainer.classList.remove('phx-navigation-loading')
})
}

liveSocket.connect()
window.liveSocket = liveSocket
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule PlausibleWeb.CustomerSupport.Site.Components.Overview do

<.styled_link
new_tab={true}
href={Routes.stats_path(PlausibleWeb.Endpoint, :stats, @site.domain, [])}
href={Routes.stats_path(PlausibleWeb.Endpoint, :dashboard, @site.domain, [])}
>
Dashboard
</.styled_link>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ defmodule PlausibleWeb.CustomerSupport.Team.Components.ConsolidatedViews do
<.td>
<.styled_link
new_tab={true}
href={Routes.stats_path(PlausibleWeb.Endpoint, :stats, consolidated_view.domain, [])}
href={
Routes.stats_path(PlausibleWeb.Endpoint, :dashboard, consolidated_view.domain, [])
}
>
Dashboard
</.styled_link>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ defmodule PlausibleWeb.CustomerSupport.Team.Components.Sites do
<.td>
<.styled_link
new_tab={true}
href={Routes.stats_path(PlausibleWeb.Endpoint, :stats, site.domain, [])}
href={Routes.stats_path(PlausibleWeb.Endpoint, :dashboard, site.domain, [])}
>
Dashboard
</.styled_link>
Expand Down
2 changes: 1 addition & 1 deletion extra/lib/plausible_web/live/verification.ex
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ defmodule PlausibleWeb.Live.Verification do
end

defp redirect_to_stats(socket) do
stats_url = Routes.stats_path(PlausibleWeb.Endpoint, :stats, socket.assigns.domain, [])
stats_url = Routes.stats_path(PlausibleWeb.Endpoint, :dashboard, socket.assigns.domain, [])
redirect(socket, to: stats_url)
end

Expand Down
Loading
Loading