@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ossy/analytics",
3
3
  "description": "Analytics module — workspace insight KPIs",
4
- "version": "3.7.0",
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.7.0",
29
- "@ossy/workspaces": "^3.6.1"
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": "622d97535abad08415963405ace2f5aaaecd7ca4"
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
- import { useSdk } from '@ossy/sdk-react'
6
- import { GetWorkspace } from '@ossy/workspaces'
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="h2">{value}</Text>
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
- {(workspaceKpis.status === 'loading' || status === 'loading') && (
52
- <View surface="primary" roundness="m" inset="m">
53
- <Text>{t('analytics.home.loading')}</Text>
54
- </View>
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
- <View surface="primary" roundness="m" inset="m">
59
- <Text>{t('analytics.home.workspaceKpisError')}</Text>
60
- </View>
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
- <View layout="row" gap="m">
65
- <SummaryCard label={t('analytics.home.members')} value={`${workspaceKpis.members}`} />
66
- <SummaryCard label={t('analytics.home.resources')} value={`${workspaceKpis.resources}`} />
67
- </View>
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
- <View surface="primary" roundness="m" inset="m">
72
- <Text>{t('analytics.home.error')}</Text>
73
- </View>
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
- <View layout="row" gap="m">
79
- <SummaryCard label={t('analytics.home.totalPageViews')} value={`${totalViews}`} />
80
- <SummaryCard label={t('analytics.home.trackedPages')} value={`${topPaths.length}`} />
81
- <SummaryCard label={t('analytics.home.daysWithTraffic')} value={`${dailyViews.length}`} />
82
- </View>
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
- surface="primary"
87
- roundness="m"
88
- inset="m"
89
- gap="s"
90
- style={{ border: '1px solid var(--separator)', flex: 1 }}
91
- >
92
- <Text variant="heading-secondary" as="h2" text="analytics.home.topPaths" />
93
- {topPaths.length === 0 && <Text variant="small" text="analytics.home.noPageViews" />}
94
- {topPaths.map((item) => (
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
- surface="primary"
104
- roundness="m"
105
- inset="m"
106
- gap="s"
107
- style={{ border: '1px solid var(--separator)', flex: 1 }}
108
- >
109
- <Text variant="heading-secondary" as="h2" text="analytics.home.dailyPageViews" />
110
- {dailyViews.length === 0 && <Text variant="small" text="analytics.home.noDailyData" />}
111
- {dailyViews.map((item) => (
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
  </>
@@ -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 only when signed in and entitled.
19
- * Otherwise show the sales page (no KPI / page-view loads).
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 sdk = useSdk()
24
+ const { workspace } = useShellWorkspace()
24
25
  const isAuthenticated = !!app?.isAuthenticated
25
- const { data: workspace } = sdk.read(GetWorkspace, undefined, { enabled: isAuthenticated })
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} />
@@ -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",
@@ -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",
@@ -1,28 +1,28 @@
1
- import { useEffect, useMemo, useState } from 'react'
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 [status, setStatus] = useState('loading')
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
- setStatus('loading')
24
- sdk.invoke(GetPageViewStats, payload)
25
- .then(json => {
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
- }, [sdk, workspaceId, payload])
37
+
38
+ return () => {
39
+ cancelled = true
40
+ }
41
+ }, [workspaceId, from, to, limit])
37
42
 
38
43
  return {
39
44
  status,
@@ -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
- setStatus('loading')
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
- }, [sdk, workspaceId])
35
+
36
+ return () => {
37
+ cancelled = true
38
+ }
39
+ }, [workspaceId])
29
40
 
30
41
  return {
31
42
  status,