@ossy/analytics 3.7.1 → 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.1",
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.7.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": "1ee220424da6b065034a7f52f52aa4a6ce7c88d5"
45
+ "gitHead": "14548dfc2019e7258e8c75db527acbf66fe829bd"
46
46
  }
@@ -3,16 +3,37 @@ import { Text, View, Page, useLocale } from '@ossy/design-system'
3
3
  import { usePageViewStats } from './usePageViewStats.js'
4
4
  import { useWorkspaceKpis } from './useWorkspaceKpis.js'
5
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
+ }
31
+
6
32
  function SummaryCard ({ label, value }) {
7
33
  return (
8
- <View
9
- surface="primary"
10
- roundness="m"
11
- inset="m"
12
- style={{ border: '1px solid var(--separator)' }}
13
- >
34
+ <View gap="xs" style={{ minWidth: 140, flex: '1 1 140px' }}>
14
35
  <Text variant="small" style={{ opacity: 0.75 }}>{label}</Text>
15
- <Text variant="heading-secondary" as="h2">{value}</Text>
36
+ <Text variant="heading-secondary" as="p">{value}</Text>
16
37
  </View>
17
38
  )
18
39
  }
@@ -38,6 +59,9 @@ export default function AnalyticsProductHome ({ workspace }) {
38
59
  const workspaceKpis = useWorkspaceKpis({ workspaceId: workspace?.id })
39
60
 
40
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
41
65
 
42
66
  return (
43
67
  <Page
@@ -45,72 +69,76 @@ export default function AnalyticsProductHome ({ workspace }) {
45
69
  description="analytics.home.description"
46
70
  >
47
71
  <View gap="m">
48
- {(workspaceKpis.status === 'loading' || status === 'loading') && (
49
- <View surface="primary" roundness="m" inset="m">
50
- <Text>{t('analytics.home.loading')}</Text>
51
- </View>
72
+ {pageLoading && (
73
+ <SectionPanel title="analytics.home.overview">
74
+ <View inset="m">
75
+ <Text>{t('analytics.home.loading')}</Text>
76
+ </View>
77
+ </SectionPanel>
52
78
  )}
53
79
 
54
- {workspaceKpis.status === 'error' && (
55
- <View surface="primary" roundness="m" inset="m">
56
- <Text>{t('analytics.home.workspaceKpisError')}</Text>
57
- </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>
58
86
  )}
59
87
 
60
- {workspaceKpis.status === 'success' && (
61
- <View layout="row" gap="m">
62
- <SummaryCard label={t('analytics.home.members')} value={`${workspaceKpis.members}`} />
63
- <SummaryCard label={t('analytics.home.resources')} value={`${workspaceKpis.resources}`} />
64
- </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>
65
95
  )}
66
96
 
67
- {status === 'error' && (
68
- <View surface="primary" roundness="m" inset="m">
69
- <Text>{t('analytics.home.error')}</Text>
70
- </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>
71
103
  )}
72
104
 
73
- {status === 'success' && (
105
+ {!pageLoading && status === 'success' && (
74
106
  <>
75
- <View layout="row" gap="m">
76
- <SummaryCard label={t('analytics.home.totalPageViews')} value={`${totalViews}`} />
77
- <SummaryCard label={t('analytics.home.trackedPages')} value={`${topPaths.length}`} />
78
- <SummaryCard label={t('analytics.home.daysWithTraffic')} value={`${dailyViews.length}`} />
79
- </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>
80
114
 
81
115
  <View layout="row" gap="m" style={{ alignItems: 'flex-start' }}>
82
- <View
83
- surface="primary"
84
- roundness="m"
85
- inset="m"
86
- gap="s"
87
- style={{ border: '1px solid var(--separator)', flex: 1 }}
88
- >
89
- <Text variant="heading-secondary" as="h2" text="analytics.home.topPaths" />
90
- {topPaths.length === 0 && <Text variant="small" text="analytics.home.noPageViews" />}
91
- {topPaths.map((item) => (
92
- <View key={item.path || 'unknown'} layout="row" justifyContent="space-between">
93
- <Text variant="small">{item.path || t('analytics.home.unknownPath')}</Text>
94
- <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
+ ))}
95
126
  </View>
96
- ))}
127
+ </SectionPanel>
97
128
  </View>
98
129
 
99
- <View
100
- surface="primary"
101
- roundness="m"
102
- inset="m"
103
- gap="s"
104
- style={{ border: '1px solid var(--separator)', flex: 1 }}
105
- >
106
- <Text variant="heading-secondary" as="h2" text="analytics.home.dailyPageViews" />
107
- {dailyViews.length === 0 && <Text variant="small" text="analytics.home.noDailyData" />}
108
- {dailyViews.map((item) => (
109
- <View key={item.date} layout="row" justifyContent="space-between">
110
- <Text variant="small">{item.date}</Text>
111
- <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
+ ))}
112
140
  </View>
113
- ))}
141
+ </SectionPanel>
114
142
  </View>
115
143
  </View>
116
144
  </>
@@ -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,