@ossy/analytics 3.7.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/AnalyticsProductHome.jsx +88 -63
- package/src/analytics.page.jsx +9 -9
- package/src/en.translations.json +2 -0
- package/src/sv.translations.json +2 -0
- package/src/usePageViewStats.js +18 -13
- package/src/useWorkspaceKpis.js +16 -5
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/analytics",
|
|
3
3
|
"description": "Analytics module — workspace insight KPIs",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.8.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"README.md"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@ossy/event-store": "^3.
|
|
29
|
-
"@ossy/workspaces": "^3.
|
|
28
|
+
"@ossy/event-store": "^3.8.0",
|
|
29
|
+
"@ossy/workspaces": "^3.8.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@ossy/app": "*",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"@jest/globals": "^30.2.0",
|
|
43
43
|
"jest": "^30.2.0"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "14548dfc2019e7258e8c75db527acbf66fe829bd"
|
|
46
46
|
}
|
|
@@ -2,19 +2,38 @@ import React, { useMemo } from 'react'
|
|
|
2
2
|
import { Text, View, Page, useLocale } from '@ossy/design-system'
|
|
3
3
|
import { usePageViewStats } from './usePageViewStats.js'
|
|
4
4
|
import { useWorkspaceKpis } from './useWorkspaceKpis.js'
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
function SectionPanel ({ title, children }) {
|
|
7
|
+
return (
|
|
8
|
+
<View surface="primary" roundness="xs" style={{ minWidth: 0, overflow: 'hidden' }}>
|
|
9
|
+
<View stack bordered>
|
|
10
|
+
<View.Item
|
|
11
|
+
data-surface="primary"
|
|
12
|
+
layout="row"
|
|
13
|
+
style={{
|
|
14
|
+
alignItems: 'center',
|
|
15
|
+
justifyContent: 'space-between',
|
|
16
|
+
padding: 'var(--space-s) var(--space-m)',
|
|
17
|
+
gap: 'var(--space-m)',
|
|
18
|
+
height: '48px',
|
|
19
|
+
flexShrink: 0,
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
<Text variant="small" as="h2" style={{ fontWeight: 'bold' }} text={title} />
|
|
23
|
+
</View.Item>
|
|
24
|
+
<View.Item>
|
|
25
|
+
{children}
|
|
26
|
+
</View.Item>
|
|
27
|
+
</View>
|
|
28
|
+
</View>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
7
31
|
|
|
8
32
|
function SummaryCard ({ label, value }) {
|
|
9
33
|
return (
|
|
10
|
-
<View
|
|
11
|
-
surface="primary"
|
|
12
|
-
roundness="m"
|
|
13
|
-
inset="m"
|
|
14
|
-
style={{ border: '1px solid var(--separator)' }}
|
|
15
|
-
>
|
|
34
|
+
<View gap="xs" style={{ minWidth: 140, flex: '1 1 140px' }}>
|
|
16
35
|
<Text variant="small" style={{ opacity: 0.75 }}>{label}</Text>
|
|
17
|
-
<Text variant="heading-secondary" as="
|
|
36
|
+
<Text variant="heading-secondary" as="p">{value}</Text>
|
|
18
37
|
</View>
|
|
19
38
|
)
|
|
20
39
|
}
|
|
@@ -22,11 +41,10 @@ function SummaryCard ({ label, value }) {
|
|
|
22
41
|
/**
|
|
23
42
|
* Workspace analytics product UI. Only mount when the caller is authenticated
|
|
24
43
|
* so KPI / page-view actions do not run for anonymous sessions.
|
|
44
|
+
* Workspace comes from the page (shell snapshot) — do not re-read GetWorkspace.
|
|
25
45
|
*/
|
|
26
|
-
export default function AnalyticsProductHome () {
|
|
46
|
+
export default function AnalyticsProductHome ({ workspace }) {
|
|
27
47
|
const { t } = useLocale()
|
|
28
|
-
const sdk = useSdk()
|
|
29
|
-
const { data: workspace } = sdk.read(GetWorkspace)
|
|
30
48
|
const { from, to } = useMemo(() => {
|
|
31
49
|
const now = Date.now()
|
|
32
50
|
return { from: now - 30 * 24 * 60 * 60 * 1000, to: now }
|
|
@@ -41,6 +59,9 @@ export default function AnalyticsProductHome () {
|
|
|
41
59
|
const workspaceKpis = useWorkspaceKpis({ workspaceId: workspace?.id })
|
|
42
60
|
|
|
43
61
|
const { status, totalViews, dailyViews, topPaths } = pageViews
|
|
62
|
+
const kpisSettled = workspaceKpis.status === 'success' || workspaceKpis.status === 'error'
|
|
63
|
+
const viewsSettled = status === 'success' || status === 'error'
|
|
64
|
+
const pageLoading = !kpisSettled || !viewsSettled
|
|
44
65
|
|
|
45
66
|
return (
|
|
46
67
|
<Page
|
|
@@ -48,72 +69,76 @@ export default function AnalyticsProductHome () {
|
|
|
48
69
|
description="analytics.home.description"
|
|
49
70
|
>
|
|
50
71
|
<View gap="m">
|
|
51
|
-
{
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
|
|
72
|
+
{pageLoading && (
|
|
73
|
+
<SectionPanel title="analytics.home.overview">
|
|
74
|
+
<View inset="m">
|
|
75
|
+
<Text>{t('analytics.home.loading')}</Text>
|
|
76
|
+
</View>
|
|
77
|
+
</SectionPanel>
|
|
55
78
|
)}
|
|
56
79
|
|
|
57
|
-
{workspaceKpis.status === 'error' && (
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
|
|
80
|
+
{!pageLoading && workspaceKpis.status === 'error' && (
|
|
81
|
+
<SectionPanel title="analytics.home.overview">
|
|
82
|
+
<View inset="m">
|
|
83
|
+
<Text>{t('analytics.home.workspaceKpisError')}</Text>
|
|
84
|
+
</View>
|
|
85
|
+
</SectionPanel>
|
|
61
86
|
)}
|
|
62
87
|
|
|
63
|
-
{workspaceKpis.status === 'success' && (
|
|
64
|
-
<
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
88
|
+
{!pageLoading && workspaceKpis.status === 'success' && (
|
|
89
|
+
<SectionPanel title="analytics.home.overview">
|
|
90
|
+
<View layout="row-wrap" gap="m" inset="m">
|
|
91
|
+
<SummaryCard label={t('analytics.home.members')} value={`${workspaceKpis.members}`} />
|
|
92
|
+
<SummaryCard label={t('analytics.home.resources')} value={`${workspaceKpis.resources}`} />
|
|
93
|
+
</View>
|
|
94
|
+
</SectionPanel>
|
|
68
95
|
)}
|
|
69
96
|
|
|
70
|
-
{status === 'error' && (
|
|
71
|
-
<
|
|
72
|
-
<
|
|
73
|
-
|
|
97
|
+
{!pageLoading && status === 'error' && (
|
|
98
|
+
<SectionPanel title="analytics.home.traffic">
|
|
99
|
+
<View inset="m">
|
|
100
|
+
<Text>{t('analytics.home.error')}</Text>
|
|
101
|
+
</View>
|
|
102
|
+
</SectionPanel>
|
|
74
103
|
)}
|
|
75
104
|
|
|
76
|
-
{status === 'success' && (
|
|
105
|
+
{!pageLoading && status === 'success' && (
|
|
77
106
|
<>
|
|
78
|
-
<
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
107
|
+
<SectionPanel title="analytics.home.traffic">
|
|
108
|
+
<View layout="row-wrap" gap="m" inset="m">
|
|
109
|
+
<SummaryCard label={t('analytics.home.totalPageViews')} value={`${totalViews}`} />
|
|
110
|
+
<SummaryCard label={t('analytics.home.trackedPages')} value={`${topPaths.length}`} />
|
|
111
|
+
<SummaryCard label={t('analytics.home.daysWithTraffic')} value={`${dailyViews.length}`} />
|
|
112
|
+
</View>
|
|
113
|
+
</SectionPanel>
|
|
83
114
|
|
|
84
115
|
<View layout="row" gap="m" style={{ alignItems: 'flex-start' }}>
|
|
85
|
-
<View
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<View key={item.path || 'unknown'} layout="row" justifyContent="space-between">
|
|
96
|
-
<Text variant="small">{item.path || t('analytics.home.unknownPath')}</Text>
|
|
97
|
-
<Text variant="small">{item.views}</Text>
|
|
116
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
117
|
+
<SectionPanel title="analytics.home.topPaths">
|
|
118
|
+
<View gap="s" inset="m">
|
|
119
|
+
{topPaths.length === 0 && <Text variant="small" text="analytics.home.noPageViews" />}
|
|
120
|
+
{topPaths.map((item) => (
|
|
121
|
+
<View key={item.path || 'unknown'} layout="row" justifyContent="space-between">
|
|
122
|
+
<Text variant="small">{item.path || t('analytics.home.unknownPath')}</Text>
|
|
123
|
+
<Text variant="small">{item.views}</Text>
|
|
124
|
+
</View>
|
|
125
|
+
))}
|
|
98
126
|
</View>
|
|
99
|
-
|
|
127
|
+
</SectionPanel>
|
|
100
128
|
</View>
|
|
101
129
|
|
|
102
|
-
<View
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
<View key={item.date} layout="row" justifyContent="space-between">
|
|
113
|
-
<Text variant="small">{item.date}</Text>
|
|
114
|
-
<Text variant="small">{item.views}</Text>
|
|
130
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
131
|
+
<SectionPanel title="analytics.home.dailyPageViews">
|
|
132
|
+
<View gap="s" inset="m">
|
|
133
|
+
{dailyViews.length === 0 && <Text variant="small" text="analytics.home.noDailyData" />}
|
|
134
|
+
{dailyViews.map((item) => (
|
|
135
|
+
<View key={item.date} layout="row" justifyContent="space-between">
|
|
136
|
+
<Text variant="small">{item.date}</Text>
|
|
137
|
+
<Text variant="small">{item.views}</Text>
|
|
138
|
+
</View>
|
|
139
|
+
))}
|
|
115
140
|
</View>
|
|
116
|
-
|
|
141
|
+
</SectionPanel>
|
|
117
142
|
</View>
|
|
118
143
|
</View>
|
|
119
144
|
</>
|
package/src/analytics.page.jsx
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { useApp } from '@ossy/app/shell'
|
|
3
|
-
import { GetWorkspace } from '@ossy/workspaces'
|
|
2
|
+
import { useApp, useShellWorkspace } from '@ossy/app/shell'
|
|
4
3
|
import { isServiceEntitled } from '@ossy/workspaces/entitlements'
|
|
5
|
-
import { useSdk } from '@ossy/sdk-react'
|
|
6
4
|
import AnalyticsSalesPage from './AnalyticsSalesPage.jsx'
|
|
7
5
|
import AnalyticsProductHome from './AnalyticsProductHome.jsx'
|
|
8
6
|
|
|
@@ -14,19 +12,21 @@ export const metadata = {
|
|
|
14
12
|
},
|
|
15
13
|
}
|
|
16
14
|
|
|
15
|
+
const ANALYTICS_SERVICE = '@ossy/analytics'
|
|
16
|
+
|
|
17
17
|
/**
|
|
18
|
-
* Analytics home — product KPIs
|
|
19
|
-
*
|
|
18
|
+
* Analytics home — product KPIs when signed in and entitled, otherwise sales.
|
|
19
|
+
* Entitlement comes from the shell workspace snapshot (SSR bootstrap / cookie)
|
|
20
|
+
* so the dashboard does not flash the sales page on navigation.
|
|
20
21
|
*/
|
|
21
22
|
export default function AnalyticsPage () {
|
|
22
23
|
const app = useApp()
|
|
23
|
-
const
|
|
24
|
+
const { workspace } = useShellWorkspace()
|
|
24
25
|
const isAuthenticated = !!app?.isAuthenticated
|
|
25
|
-
const
|
|
26
|
-
const entitled = isServiceEntitled(workspace?.services, '@ossy/analytics')
|
|
26
|
+
const entitled = isServiceEntitled(workspace?.services, ANALYTICS_SERVICE)
|
|
27
27
|
|
|
28
28
|
if (isAuthenticated && entitled) {
|
|
29
|
-
return <AnalyticsProductHome />
|
|
29
|
+
return <AnalyticsProductHome workspace={workspace} />
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
return <AnalyticsSalesPage isAuthenticated={isAuthenticated} />
|
package/src/en.translations.json
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
"analytics/home.documentTitle": "Analytics",
|
|
3
3
|
"analytics.home.title": "Analytics",
|
|
4
4
|
"analytics.home.description": "Workspace insight KPIs for members, resources, and traffic.",
|
|
5
|
+
"analytics.home.overview": "Overview",
|
|
6
|
+
"analytics.home.traffic": "Traffic",
|
|
5
7
|
"analytics.home.loading": "Loading analytics...",
|
|
6
8
|
"analytics.home.error": "Could not load analytics data.",
|
|
7
9
|
"analytics.home.members": "Members",
|
package/src/sv.translations.json
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
"analytics/home.documentTitle": "Analys",
|
|
3
3
|
"analytics.home.title": "Analys",
|
|
4
4
|
"analytics.home.description": "Workspace-KPIer för medlemmar, resurser och trafik.",
|
|
5
|
+
"analytics.home.overview": "Översikt",
|
|
6
|
+
"analytics.home.traffic": "Trafik",
|
|
5
7
|
"analytics.home.loading": "Laddar analys...",
|
|
6
8
|
"analytics.home.error": "Kunde inte ladda analysdata.",
|
|
7
9
|
"analytics.home.members": "Medlemmar",
|
package/src/usePageViewStats.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { GetPageViewStats } from '@ossy/resources'
|
|
3
3
|
import { useSdk } from '@ossy/sdk-react'
|
|
4
4
|
|
|
5
5
|
export function usePageViewStats({ workspaceId, from, to, limit = 10 }) {
|
|
6
6
|
const sdk = useSdk()
|
|
7
|
-
const
|
|
7
|
+
const invokeRef = useRef(sdk.invoke)
|
|
8
|
+
invokeRef.current = sdk.invoke
|
|
9
|
+
|
|
10
|
+
const [status, setStatus] = useState('idle')
|
|
8
11
|
const [data, setData] = useState({
|
|
9
12
|
totalViews: 0,
|
|
10
13
|
dailyViews: [],
|
|
11
14
|
topPaths: [],
|
|
12
15
|
})
|
|
13
16
|
|
|
14
|
-
const payload = useMemo(() => ({
|
|
15
|
-
from,
|
|
16
|
-
to,
|
|
17
|
-
limit,
|
|
18
|
-
}), [from, to, limit])
|
|
19
|
-
|
|
20
17
|
useEffect(() => {
|
|
21
|
-
if (!workspaceId) return
|
|
18
|
+
if (!workspaceId) return undefined
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
let cancelled = false
|
|
21
|
+
setStatus((current) => (current === 'success' ? current : 'loading'))
|
|
22
|
+
|
|
23
|
+
invokeRef.current(GetPageViewStats, { from, to, limit })
|
|
24
|
+
.then((json) => {
|
|
25
|
+
if (cancelled) return
|
|
26
26
|
setData({
|
|
27
27
|
totalViews: json?.totalViews || 0,
|
|
28
28
|
dailyViews: json?.dailyViews || [],
|
|
@@ -31,9 +31,14 @@ export function usePageViewStats({ workspaceId, from, to, limit = 10 }) {
|
|
|
31
31
|
setStatus('success')
|
|
32
32
|
})
|
|
33
33
|
.catch(() => {
|
|
34
|
+
if (cancelled) return
|
|
34
35
|
setStatus('error')
|
|
35
36
|
})
|
|
36
|
-
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
cancelled = true
|
|
40
|
+
}
|
|
41
|
+
}, [workspaceId, from, to, limit])
|
|
37
42
|
|
|
38
43
|
return {
|
|
39
44
|
status,
|
package/src/useWorkspaceKpis.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { useSdk } from '@ossy/sdk-react'
|
|
3
3
|
import { metadata as GetWorkspaceKpis } from './get-workspace-kpis.action.js'
|
|
4
4
|
|
|
5
5
|
export function useWorkspaceKpis({ workspaceId }) {
|
|
6
6
|
const sdk = useSdk()
|
|
7
|
+
const invokeRef = useRef(sdk.invoke)
|
|
8
|
+
invokeRef.current = sdk.invoke
|
|
9
|
+
|
|
7
10
|
const [status, setStatus] = useState('idle')
|
|
8
11
|
const [data, setData] = useState({
|
|
9
12
|
members: 0,
|
|
@@ -11,11 +14,14 @@ export function useWorkspaceKpis({ workspaceId }) {
|
|
|
11
14
|
})
|
|
12
15
|
|
|
13
16
|
useEffect(() => {
|
|
14
|
-
if (!workspaceId) return
|
|
17
|
+
if (!workspaceId) return undefined
|
|
18
|
+
|
|
19
|
+
let cancelled = false
|
|
20
|
+
setStatus((current) => (current === 'success' ? current : 'loading'))
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
sdk.invoke(GetWorkspaceKpis, {})
|
|
22
|
+
invokeRef.current(GetWorkspaceKpis, {})
|
|
18
23
|
.then((json) => {
|
|
24
|
+
if (cancelled) return
|
|
19
25
|
setData({
|
|
20
26
|
members: json?.members || 0,
|
|
21
27
|
resources: json?.resources || 0,
|
|
@@ -23,9 +29,14 @@ export function useWorkspaceKpis({ workspaceId }) {
|
|
|
23
29
|
setStatus('success')
|
|
24
30
|
})
|
|
25
31
|
.catch(() => {
|
|
32
|
+
if (cancelled) return
|
|
26
33
|
setStatus('error')
|
|
27
34
|
})
|
|
28
|
-
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
cancelled = true
|
|
38
|
+
}
|
|
39
|
+
}, [workspaceId])
|
|
29
40
|
|
|
30
41
|
return {
|
|
31
42
|
status,
|