@byline/admin 4.15.0 → 4.16.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.
Files changed (29) hide show
  1. package/dist/abilities.js +2 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +1 -0
  4. package/dist/modules/analytics/abilities.d.ts +14 -0
  5. package/dist/modules/analytics/abilities.js +21 -0
  6. package/dist/modules/analytics/components/dashboard.d.ts +20 -0
  7. package/dist/modules/analytics/components/dashboard.js +308 -0
  8. package/dist/modules/analytics/components/dashboard.module.js +29 -0
  9. package/dist/modules/analytics/components/dashboard.test.node.d.ts +8 -0
  10. package/dist/modules/analytics/components/dashboard_module.css +268 -0
  11. package/dist/modules/analytics/components/timeseries.d.ts +59 -0
  12. package/dist/modules/analytics/components/timeseries.js +253 -0
  13. package/dist/modules/analytics/components/timeseries.module.js +17 -0
  14. package/dist/modules/analytics/components/timeseries_module.css +126 -0
  15. package/dist/modules/analytics/index.d.ts +9 -0
  16. package/dist/modules/analytics/index.js +1 -0
  17. package/dist/modules/analytics/types.d.ts +20 -0
  18. package/dist/modules/analytics/types.js +1 -0
  19. package/package.json +17 -5
  20. package/src/abilities.ts +2 -0
  21. package/src/index.ts +1 -0
  22. package/src/modules/analytics/abilities.ts +33 -0
  23. package/src/modules/analytics/components/dashboard.module.css +330 -0
  24. package/src/modules/analytics/components/dashboard.test.node.ts +193 -0
  25. package/src/modules/analytics/components/dashboard.tsx +371 -0
  26. package/src/modules/analytics/components/timeseries.module.css +151 -0
  27. package/src/modules/analytics/components/timeseries.tsx +332 -0
  28. package/src/modules/analytics/index.ts +14 -0
  29. package/src/modules/analytics/types.ts +31 -0
package/dist/abilities.js CHANGED
@@ -2,10 +2,12 @@ import { registerAdminActivityAbilities } from "./modules/admin-activity/abiliti
2
2
  import { registerAdminPermissionsAbilities } from "./modules/admin-permissions/abilities.js";
3
3
  import { registerAdminRolesAbilities } from "./modules/admin-roles/abilities.js";
4
4
  import { registerAdminUsersAbilities } from "./modules/admin-users/abilities.js";
5
+ import { registerAnalyticsAbilities } from "./modules/analytics/abilities.js";
5
6
  function registerAdminAbilities(registry) {
6
7
  registerAdminUsersAbilities(registry);
7
8
  registerAdminRolesAbilities(registry);
8
9
  registerAdminPermissionsAbilities(registry);
9
10
  registerAdminActivityAbilities(registry);
11
+ registerAnalyticsAbilities(registry);
10
12
  }
11
13
  export { registerAdminAbilities };
package/dist/index.d.ts CHANGED
@@ -28,5 +28,6 @@ export * from './modules/admin-activity/index.js';
28
28
  export * from './modules/admin-permissions/index.js';
29
29
  export * from './modules/admin-roles/index.js';
30
30
  export * from './modules/admin-users/index.js';
31
+ export * from './modules/analytics/index.js';
31
32
  export * from './modules/auth/index.js';
32
33
  export type { AdminStore } from './store.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from "./modules/admin-activity/index.js";
3
3
  export * from "./modules/admin-permissions/index.js";
4
4
  export * from "./modules/admin-roles/index.js";
5
5
  export * from "./modules/admin-users/index.js";
6
+ export * from "./modules/analytics/index.js";
6
7
  export * from "./modules/auth/index.js";
7
8
  export { registerAdminAbilities } from "./abilities.js";
8
9
  export { assertAdminActor, requireAdminActor } from "./lib/assert-admin-actor.js";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { AbilityRegistry } from '@byline/auth';
9
+ export declare const ANALYTICS_ABILITIES: {
10
+ readonly read: 'analytics.read';
11
+ readonly maintain: 'analytics.maintain';
12
+ };
13
+ export type AnalyticsAbilityKey = (typeof ANALYTICS_ABILITIES)[keyof typeof ANALYTICS_ABILITIES];
14
+ export declare function registerAnalyticsAbilities(registry: AbilityRegistry): void;
@@ -0,0 +1,21 @@
1
+ const ANALYTICS_ABILITIES = {
2
+ read: 'analytics.read',
3
+ maintain: 'analytics.maintain'
4
+ };
5
+ function registerAnalyticsAbilities(registry) {
6
+ registry.register({
7
+ key: ANALYTICS_ABILITIES.read,
8
+ label: 'Read analytics',
9
+ description: 'View installation-level page, visitor, download, referrer, and country totals.',
10
+ group: 'analytics',
11
+ source: 'admin'
12
+ });
13
+ registry.register({
14
+ key: ANALYTICS_ABILITIES.maintain,
15
+ label: 'Maintain analytics',
16
+ description: 'Delete retained analytics events and rebuild affected daily aggregates.',
17
+ group: 'analytics',
18
+ source: 'admin'
19
+ });
20
+ }
21
+ export { ANALYTICS_ABILITIES, registerAnalyticsAbilities };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type React from 'react';
9
+ import type { AnalyticsDashboardData, AnalyticsDashboardPeriod } from '../types.js';
10
+ export interface AnalyticsDashboardProps {
11
+ data: AnalyticsDashboardData;
12
+ period: AnalyticsDashboardPeriod;
13
+ onPeriodChange(period: AnalyticsDashboardPeriod): void;
14
+ }
15
+ export declare function AnalyticsDashboard({ data, period, onPeriodChange, }: AnalyticsDashboardProps): React.JSX.Element;
16
+ /** Never collapse the bar entirely: a visible sliver still encodes "smallest". */
17
+ export declare function shareWidth(value: number, ceiling: number): number;
18
+ export declare function formatShare(part: number, whole: number, locale: string): string;
19
+ /** Return the retained boundary only when it truncates the selected range. */
20
+ export declare function partialCoverageFrom(rangeFrom: string, coverageFrom: string | null): string | undefined;
@@ -0,0 +1,308 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { ANALYTICS_DASHBOARD_PERIODS, ANALYTICS_OVERFLOW_KEY, isAnalyticsDashboardPeriod } from "@byline/analytics/config";
5
+ import { ANALYTICS_IGNORE_STORAGE_KEY } from "@byline/analytics-agent";
6
+ import { useTranslation } from "@byline/i18n/react";
7
+ import { Button, Card, Container, Section, Select } from "@byline/ui/react";
8
+ import clsx from "clsx";
9
+ import dashboard_module from "./dashboard.module.js";
10
+ import { AnalyticsTimeseries, resolveAnalyticsChartGranularity } from "./timeseries.js";
11
+ function AnalyticsDashboard({ data, period, onPeriodChange }) {
12
+ const { locale, t } = useTranslation('byline-admin');
13
+ const [excluded, setExcluded] = useState(false);
14
+ const numbers = useMemo(()=>new Intl.NumberFormat(locale), [
15
+ locale
16
+ ]);
17
+ useEffect(()=>{
18
+ try {
19
+ setExcluded(null != localStorage.getItem(ANALYTICS_IGNORE_STORAGE_KEY));
20
+ } catch {
21
+ setExcluded(false);
22
+ }
23
+ }, []);
24
+ const toggleExclusion = ()=>{
25
+ try {
26
+ if (excluded) localStorage.removeItem(ANALYTICS_IGNORE_STORAGE_KEY);
27
+ else localStorage.setItem(ANALYTICS_IGNORE_STORAGE_KEY, '1');
28
+ setExcluded(!excluded);
29
+ } catch {}
30
+ };
31
+ const periodItems = ANALYTICS_DASHBOARD_PERIODS.map((value)=>({
32
+ value: String(value),
33
+ label: 'number' == typeof value ? t('analytics.period.days', {
34
+ count: value
35
+ }) : t(`analytics.period.${value}`)
36
+ }));
37
+ const { views, visitors, downloads } = data.summary;
38
+ const days = data.summary.timeseries.length;
39
+ const chartGranularity = resolveAnalyticsChartGranularity(period, days);
40
+ return /*#__PURE__*/ jsx(Section, {
41
+ children: /*#__PURE__*/ jsxs(Container, {
42
+ children: [
43
+ /*#__PURE__*/ jsxs("header", {
44
+ className: clsx('byline-analytics-header', dashboard_module.header),
45
+ children: [
46
+ /*#__PURE__*/ jsxs("div", {
47
+ children: [
48
+ /*#__PURE__*/ jsx("h1", {
49
+ className: clsx('byline-analytics-title', dashboard_module.title),
50
+ children: t('analytics.title')
51
+ }),
52
+ /*#__PURE__*/ jsx("p", {
53
+ className: clsx('muted', 'byline-analytics-help', dashboard_module.help),
54
+ children: t('analytics.dailyUniquesHelp')
55
+ })
56
+ ]
57
+ }),
58
+ /*#__PURE__*/ jsxs("div", {
59
+ className: clsx('byline-analytics-controls', dashboard_module.controls),
60
+ children: [
61
+ /*#__PURE__*/ jsx(Select, {
62
+ id: "analytics-period",
63
+ name: "analytics-period",
64
+ "aria-label": t('analytics.period.label'),
65
+ size: "sm",
66
+ value: String(period),
67
+ items: periodItems,
68
+ onValueChange: (value)=>{
69
+ const next = 'ytd' === value || 'all' === value ? value : Number(value);
70
+ if (isAnalyticsDashboardPeriod(next)) onPeriodChange(next);
71
+ }
72
+ }),
73
+ /*#__PURE__*/ jsx(Button, {
74
+ type: "button",
75
+ size: "sm",
76
+ "aria-pressed": excluded,
77
+ title: t('analytics.exclusion.help'),
78
+ onClick: toggleExclusion,
79
+ children: excluded ? t('analytics.exclusion.include') : t('analytics.exclusion.exclude')
80
+ })
81
+ ]
82
+ })
83
+ ]
84
+ }),
85
+ /*#__PURE__*/ jsxs("div", {
86
+ className: clsx('byline-analytics-stats', dashboard_module.stats),
87
+ children: [
88
+ /*#__PURE__*/ jsx(StatTile, {
89
+ tone: "views",
90
+ label: t('analytics.stats.views'),
91
+ value: numbers.format(views),
92
+ foot: t('analytics.stats.perDay', {
93
+ count: 0 === days ? 0 : Math.round(views / days)
94
+ })
95
+ }),
96
+ /*#__PURE__*/ jsx(StatTile, {
97
+ tone: "visitors",
98
+ label: t('analytics.stats.dailyUniques'),
99
+ value: numbers.format(visitors),
100
+ foot: t('analytics.stats.sumOfDays', {
101
+ count: days
102
+ })
103
+ }),
104
+ /*#__PURE__*/ jsx(StatTile, {
105
+ tone: "downloads",
106
+ label: t('analytics.stats.downloads'),
107
+ value: numbers.format(downloads),
108
+ foot: t('analytics.stats.shareOfViews', {
109
+ share: formatShare(downloads, views, locale)
110
+ })
111
+ })
112
+ ]
113
+ }),
114
+ /*#__PURE__*/ jsxs(Card, {
115
+ className: clsx('byline-analytics-chart-card', dashboard_module.chartCard),
116
+ children: [
117
+ /*#__PURE__*/ jsx(Card.Header, {
118
+ children: /*#__PURE__*/ jsx(Card.Title, {
119
+ children: 'day' === chartGranularity ? t('analytics.chart.perDay') : 'seven-day' === chartGranularity ? t('analytics.chart.perSevenDays') : t('analytics.chart.perMonth')
120
+ })
121
+ }),
122
+ /*#__PURE__*/ jsx(Card.Content, {
123
+ children: /*#__PURE__*/ jsx(AnalyticsTimeseries, {
124
+ days: data.summary.timeseries,
125
+ granularity: chartGranularity,
126
+ locale: locale
127
+ })
128
+ })
129
+ ]
130
+ }),
131
+ /*#__PURE__*/ jsxs("div", {
132
+ className: clsx('byline-analytics-lists', dashboard_module.lists),
133
+ children: [
134
+ /*#__PURE__*/ jsx(RankedList, {
135
+ title: t('analytics.sections.pages'),
136
+ caption: t('analytics.columns.viewsAndUniques'),
137
+ tone: "views",
138
+ locale: locale,
139
+ rows: data.pages.rows.map(toPathRow),
140
+ total: data.pages.total,
141
+ coverageFrom: partialCoverageFrom(data.range.from, data.coverage.pathsFrom)
142
+ }),
143
+ /*#__PURE__*/ jsxs("div", {
144
+ className: dashboard_module.stack,
145
+ children: [
146
+ /*#__PURE__*/ jsx(RankedList, {
147
+ title: t('analytics.sections.referrers'),
148
+ tone: "visitors",
149
+ locale: locale,
150
+ total: data.referrers.total,
151
+ coverageFrom: partialCoverageFrom(data.range.from, data.coverage.referrersFrom),
152
+ rows: data.referrers.rows.map((row)=>({
153
+ key: row.referrerHost,
154
+ label: row.referrerHost,
155
+ value: row.views,
156
+ visitors: row.visitors,
157
+ overflow: row.referrerHost === ANALYTICS_OVERFLOW_KEY
158
+ }))
159
+ }),
160
+ /*#__PURE__*/ jsx(RankedList, {
161
+ title: t('analytics.sections.countries'),
162
+ tone: "visitors",
163
+ locale: locale,
164
+ rows: data.countries.map((row)=>({
165
+ key: row.country,
166
+ label: row.country,
167
+ value: row.views,
168
+ visitors: row.visitors,
169
+ overflow: false
170
+ }))
171
+ })
172
+ ]
173
+ })
174
+ ]
175
+ }),
176
+ /*#__PURE__*/ jsx(RankedList, {
177
+ title: t('analytics.sections.downloads'),
178
+ caption: t('analytics.columns.clicksAndUniques'),
179
+ tone: "downloads",
180
+ locale: locale,
181
+ rows: data.downloads.rows.map(toPathRow),
182
+ total: data.downloads.total,
183
+ coverageFrom: partialCoverageFrom(data.range.from, data.coverage.pathsFrom)
184
+ })
185
+ ]
186
+ })
187
+ });
188
+ }
189
+ const TONE_TILE = {
190
+ views: dashboard_module.toneViews,
191
+ visitors: dashboard_module.toneVisitors,
192
+ downloads: dashboard_module.toneDownloads
193
+ };
194
+ const TONE_BAR = {
195
+ views: dashboard_module.barViews,
196
+ visitors: dashboard_module.barVisitors,
197
+ downloads: dashboard_module.barDownloads
198
+ };
199
+ function StatTile({ tone, label, value, foot }) {
200
+ return /*#__PURE__*/ jsxs("div", {
201
+ className: clsx('byline-analytics-stat', dashboard_module.stat, TONE_TILE[tone]),
202
+ children: [
203
+ /*#__PURE__*/ jsx("span", {
204
+ className: clsx('byline-analytics-stat-label', dashboard_module.statLabel),
205
+ children: label
206
+ }),
207
+ /*#__PURE__*/ jsx("span", {
208
+ className: clsx('byline-analytics-stat-value', dashboard_module.statValue),
209
+ children: value
210
+ }),
211
+ /*#__PURE__*/ jsx("span", {
212
+ className: clsx('byline-analytics-stat-foot', dashboard_module.statFoot),
213
+ children: foot
214
+ })
215
+ ]
216
+ });
217
+ }
218
+ function toPathRow(row) {
219
+ return {
220
+ key: row.path,
221
+ label: row.path,
222
+ value: row.views,
223
+ visitors: row.visitors,
224
+ overflow: row.path === ANALYTICS_OVERFLOW_KEY
225
+ };
226
+ }
227
+ function RankedList({ title, caption, rows, tone, locale, total, coverageFrom }) {
228
+ const { t } = useTranslation('byline-admin');
229
+ const numbers = useMemo(()=>new Intl.NumberFormat(locale), [
230
+ locale
231
+ ]);
232
+ const ceiling = Math.max(1, ...rows.map((row)=>row.value));
233
+ const truncated = null != total && total > rows.length;
234
+ const coverageDate = useMemo(()=>new Intl.DateTimeFormat(locale, {
235
+ dateStyle: 'medium',
236
+ timeZone: 'UTC'
237
+ }), [
238
+ locale
239
+ ]);
240
+ const description = [
241
+ truncated ? t('analytics.topOf', {
242
+ shown: rows.length,
243
+ total
244
+ }) : caption,
245
+ null == coverageFrom ? void 0 : t('analytics.coverage.since', {
246
+ date: coverageDate.format(new Date(`${coverageFrom}T00:00:00.000Z`))
247
+ })
248
+ ].filter((value)=>null != value).join(' · ');
249
+ return /*#__PURE__*/ jsxs(Card, {
250
+ className: clsx('byline-analytics-list', dashboard_module.list),
251
+ children: [
252
+ /*#__PURE__*/ jsxs(Card.Header, {
253
+ children: [
254
+ /*#__PURE__*/ jsx(Card.Title, {
255
+ children: title
256
+ }),
257
+ description.length > 0 && /*#__PURE__*/ jsx(Card.Description, {
258
+ children: description
259
+ })
260
+ ]
261
+ }),
262
+ /*#__PURE__*/ jsx(Card.Content, {
263
+ children: 0 === rows.length ? /*#__PURE__*/ jsx("p", {
264
+ className: "muted",
265
+ children: t('analytics.empty')
266
+ }) : /*#__PURE__*/ jsx("ol", {
267
+ className: clsx('byline-analytics-ranking', dashboard_module.ranking),
268
+ children: rows.map((row)=>/*#__PURE__*/ jsxs("li", {
269
+ className: clsx('byline-analytics-ranking-row', dashboard_module.rankingRow, TONE_BAR[tone], row.overflow && dashboard_module.rankingOverflow),
270
+ style: {
271
+ '--byline-analytics-share': `${shareWidth(row.value, ceiling)}%`
272
+ },
273
+ children: [
274
+ /*#__PURE__*/ jsx("span", {
275
+ className: dashboard_module.rankingLabel,
276
+ title: row.key,
277
+ children: row.overflow ? t('analytics.overflow') : row.label
278
+ }),
279
+ /*#__PURE__*/ jsx("span", {
280
+ className: dashboard_module.rankingValue,
281
+ children: numbers.format(row.value)
282
+ }),
283
+ /*#__PURE__*/ jsx("span", {
284
+ className: dashboard_module.rankingVisitors,
285
+ children: numbers.format(row.visitors)
286
+ })
287
+ ]
288
+ }, row.key))
289
+ })
290
+ })
291
+ ]
292
+ });
293
+ }
294
+ function shareWidth(value, ceiling) {
295
+ if (!Number.isFinite(value) || value <= 0 || ceiling <= 0) return 0;
296
+ return Math.max(3, Math.min(100, value / ceiling * 100));
297
+ }
298
+ function formatShare(part, whole, locale) {
299
+ const percent = new Intl.NumberFormat(locale, {
300
+ style: 'percent',
301
+ maximumFractionDigits: 1
302
+ });
303
+ return percent.format(whole <= 0 ? 0 : part / whole);
304
+ }
305
+ function partialCoverageFrom(rangeFrom, coverageFrom) {
306
+ return null != coverageFrom && coverageFrom > rangeFrom ? coverageFrom : void 0;
307
+ }
308
+ export { AnalyticsDashboard, formatShare, partialCoverageFrom, shareWidth };
@@ -0,0 +1,29 @@
1
+ import "./dashboard_module.css";
2
+ const dashboard_module = {
3
+ header: "header-zAoAGd",
4
+ title: "title-ZhxB2D",
5
+ help: "help-HT9JOI",
6
+ chartCard: "chartCard-NaXVMm",
7
+ list: "list-sR__nc",
8
+ controls: "controls-_Alpbi",
9
+ stats: "stats-u3kcTH",
10
+ stat: "stat-KhSBeS",
11
+ statLabel: "statLabel-X0_tdu",
12
+ statValue: "statValue-D32ine",
13
+ statFoot: "statFoot-yvtZiS",
14
+ toneViews: "toneViews-QGWabX",
15
+ toneVisitors: "toneVisitors-GTLZDo",
16
+ toneDownloads: "toneDownloads-WuDN7n",
17
+ lists: "lists-BLqFfy",
18
+ stack: "stack-Yzch8f",
19
+ ranking: "ranking-yfNSnw",
20
+ rankingRow: "rankingRow-CH1cHn",
21
+ barViews: "barViews-E6VuaR",
22
+ barVisitors: "barVisitors-eIANXw",
23
+ barDownloads: "barDownloads-Z50NxT",
24
+ rankingOverflow: "rankingOverflow-hUQMKU",
25
+ rankingLabel: "rankingLabel-wsuniC",
26
+ rankingValue: "rankingValue-i_vmgf",
27
+ rankingVisitors: "rankingVisitors-NZl_jj"
28
+ };
29
+ export default dashboard_module;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};