@byline/admin 5.1.3 → 5.1.4
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/dist/modules/analytics/components/dashboard.d.ts +13 -3
- package/dist/modules/analytics/components/dashboard.js +86 -68
- package/dist/modules/analytics/components/dashboard.module.js +10 -1
- package/dist/modules/analytics/components/dashboard_module.css +57 -0
- package/dist/modules/analytics/components/ranked-list-modal.d.ts +32 -0
- package/dist/modules/analytics/components/ranked-list-modal.js +145 -0
- package/dist/modules/analytics/components/ranking.d.ts +45 -0
- package/dist/modules/analytics/components/ranking.js +71 -0
- package/dist/modules/analytics/components/ranking.test.node.d.ts +8 -0
- package/package.json +7 -7
- package/src/modules/analytics/components/dashboard.module.css +69 -0
- package/src/modules/analytics/components/dashboard.tsx +105 -83
- package/src/modules/analytics/components/ranked-list-modal.test.tsx +154 -0
- package/src/modules/analytics/components/ranked-list-modal.tsx +159 -0
- package/src/modules/analytics/components/ranking.test.node.ts +43 -0
- package/src/modules/analytics/components/ranking.tsx +122 -0
|
@@ -6,15 +6,25 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import type React from 'react';
|
|
9
|
+
import type { AnalyticsPathTotal, AnalyticsRankedTotals, AnalyticsReferrerTotal } from '@byline/analytics';
|
|
9
10
|
import type { AnalyticsDashboardData, AnalyticsDashboardPeriod } from '../types.js';
|
|
11
|
+
export { shareWidth } from './ranking.js';
|
|
12
|
+
/** Rows each ranked card shows before offering the full list. */
|
|
13
|
+
export declare const ANALYTICS_PREVIEW_ROWS = 10;
|
|
14
|
+
/** The truncated lists a host can fetch in full on demand. */
|
|
15
|
+
export type AnalyticsFullListKind = 'pages' | 'downloads' | 'referrers';
|
|
10
16
|
export interface AnalyticsDashboardProps {
|
|
11
17
|
data: AnalyticsDashboardData;
|
|
12
18
|
period: AnalyticsDashboardPeriod;
|
|
13
19
|
onPeriodChange(period: AnalyticsDashboardPeriod): void;
|
|
20
|
+
/**
|
|
21
|
+
* Fetch a larger top-N for one of the truncated lists when its "View all"
|
|
22
|
+
* modal opens. Countries are always complete and never use this. Omit it
|
|
23
|
+
* and those cards still show their preview, without the action.
|
|
24
|
+
*/
|
|
25
|
+
loadFullList?(kind: AnalyticsFullListKind): Promise<AnalyticsRankedTotals<AnalyticsPathTotal | AnalyticsReferrerTotal>>;
|
|
14
26
|
}
|
|
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;
|
|
27
|
+
export declare function AnalyticsDashboard({ data, period, onPeriodChange, loadFullList, }: AnalyticsDashboardProps): React.JSX.Element;
|
|
18
28
|
export declare function formatShare(part: number, whole: number, locale: string): string;
|
|
19
29
|
/** Return the retained boundary only when it truncates the selected range. */
|
|
20
30
|
export declare function partialCoverageFrom(rangeFrom: string, coverageFrom: string | null): string | undefined;
|
|
@@ -4,11 +4,14 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
4
4
|
import { ANALYTICS_DASHBOARD_PERIODS, ANALYTICS_OVERFLOW_KEY, isAnalyticsDashboardPeriod } from "@byline/analytics/config";
|
|
5
5
|
import { ANALYTICS_IGNORE_STORAGE_KEY } from "@byline/analytics-agent";
|
|
6
6
|
import { useTranslation } from "@byline/i18n/react";
|
|
7
|
-
import { Button, Card, Container, Section, Select } from "@byline/ui/react";
|
|
7
|
+
import { Button, Card, Container, Section, Select, useModal } from "@byline/ui/react";
|
|
8
8
|
import clsx from "clsx";
|
|
9
9
|
import dashboard_module from "./dashboard.module.js";
|
|
10
|
+
import { RankedListModal } from "./ranked-list-modal.js";
|
|
11
|
+
import { RankingRows, regionName, shareWidth } from "./ranking.js";
|
|
10
12
|
import { AnalyticsTimeseries, resolveAnalyticsChartGranularity } from "./timeseries.js";
|
|
11
|
-
|
|
13
|
+
const ANALYTICS_PREVIEW_ROWS = 10;
|
|
14
|
+
function AnalyticsDashboard({ data, period, onPeriodChange, loadFullList }) {
|
|
12
15
|
const { locale, t } = useTranslation('byline-admin');
|
|
13
16
|
const [excluded, setExcluded] = useState(false);
|
|
14
17
|
const numbers = useMemo(()=>new Intl.NumberFormat(locale), [
|
|
@@ -37,6 +40,30 @@ function AnalyticsDashboard({ data, period, onPeriodChange }) {
|
|
|
37
40
|
const { views, visitors, downloads } = data.summary;
|
|
38
41
|
const days = data.summary.timeseries.length;
|
|
39
42
|
const chartGranularity = resolveAnalyticsChartGranularity(period, days);
|
|
43
|
+
const fullLists = useMemo(()=>{
|
|
44
|
+
if (!loadFullList) return {};
|
|
45
|
+
const load = (kind)=>()=>loadFullList(kind).then((result)=>({
|
|
46
|
+
rows: result.rows.map(toRankedRow),
|
|
47
|
+
total: result.total
|
|
48
|
+
}));
|
|
49
|
+
return {
|
|
50
|
+
pages: load('pages'),
|
|
51
|
+
downloads: load('downloads'),
|
|
52
|
+
referrers: load('referrers')
|
|
53
|
+
};
|
|
54
|
+
}, [
|
|
55
|
+
loadFullList
|
|
56
|
+
]);
|
|
57
|
+
const countryRows = useMemo(()=>data.countries.map((row)=>({
|
|
58
|
+
key: row.country,
|
|
59
|
+
label: regionName(row.country, locale),
|
|
60
|
+
value: row.views,
|
|
61
|
+
visitors: row.visitors,
|
|
62
|
+
overflow: false
|
|
63
|
+
})), [
|
|
64
|
+
data.countries,
|
|
65
|
+
locale
|
|
66
|
+
]);
|
|
40
67
|
return /*#__PURE__*/ jsx(Section, {
|
|
41
68
|
children: /*#__PURE__*/ jsxs(Container, {
|
|
42
69
|
children: [
|
|
@@ -136,8 +163,9 @@ function AnalyticsDashboard({ data, period, onPeriodChange }) {
|
|
|
136
163
|
caption: t('analytics.columns.viewsAndUniques'),
|
|
137
164
|
tone: "views",
|
|
138
165
|
locale: locale,
|
|
139
|
-
rows: data.pages.rows.map(
|
|
166
|
+
rows: data.pages.rows.map(toRankedRow),
|
|
140
167
|
total: data.pages.total,
|
|
168
|
+
fullList: fullLists.pages,
|
|
141
169
|
coverageFrom: partialCoverageFrom(data.range.from, data.coverage.pathsFrom)
|
|
142
170
|
}),
|
|
143
171
|
/*#__PURE__*/ jsxs("div", {
|
|
@@ -147,27 +175,21 @@ function AnalyticsDashboard({ data, period, onPeriodChange }) {
|
|
|
147
175
|
title: t('analytics.sections.referrers'),
|
|
148
176
|
tone: "visitors",
|
|
149
177
|
locale: locale,
|
|
178
|
+
rows: data.referrers.rows.map(toRankedRow),
|
|
150
179
|
total: data.referrers.total,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
key: row.referrerHost,
|
|
154
|
-
label: row.referrerHost,
|
|
155
|
-
value: row.views,
|
|
156
|
-
visitors: row.visitors,
|
|
157
|
-
overflow: row.referrerHost === ANALYTICS_OVERFLOW_KEY
|
|
158
|
-
}))
|
|
180
|
+
fullList: fullLists.referrers,
|
|
181
|
+
coverageFrom: partialCoverageFrom(data.range.from, data.coverage.referrersFrom)
|
|
159
182
|
}),
|
|
160
183
|
/*#__PURE__*/ jsx(RankedList, {
|
|
161
184
|
title: t('analytics.sections.countries'),
|
|
162
185
|
tone: "visitors",
|
|
163
186
|
locale: locale,
|
|
164
|
-
rows:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}))
|
|
187
|
+
rows: countryRows,
|
|
188
|
+
total: countryRows.length,
|
|
189
|
+
fullList: {
|
|
190
|
+
rows: countryRows,
|
|
191
|
+
total: countryRows.length
|
|
192
|
+
}
|
|
171
193
|
})
|
|
172
194
|
]
|
|
173
195
|
})
|
|
@@ -178,8 +200,9 @@ function AnalyticsDashboard({ data, period, onPeriodChange }) {
|
|
|
178
200
|
caption: t('analytics.columns.clicksAndUniques'),
|
|
179
201
|
tone: "downloads",
|
|
180
202
|
locale: locale,
|
|
181
|
-
rows: data.downloads.rows.map(
|
|
203
|
+
rows: data.downloads.rows.map(toRankedRow),
|
|
182
204
|
total: data.downloads.total,
|
|
205
|
+
fullList: fullLists.downloads,
|
|
183
206
|
coverageFrom: partialCoverageFrom(data.range.from, data.coverage.pathsFrom)
|
|
184
207
|
})
|
|
185
208
|
]
|
|
@@ -191,11 +214,6 @@ const TONE_TILE = {
|
|
|
191
214
|
visitors: dashboard_module.toneVisitors,
|
|
192
215
|
downloads: dashboard_module.toneDownloads
|
|
193
216
|
};
|
|
194
|
-
const TONE_BAR = {
|
|
195
|
-
views: dashboard_module.barViews,
|
|
196
|
-
visitors: dashboard_module.barVisitors,
|
|
197
|
-
downloads: dashboard_module.barDownloads
|
|
198
|
-
};
|
|
199
217
|
function StatTile({ tone, label, value, foot }) {
|
|
200
218
|
return /*#__PURE__*/ jsxs("div", {
|
|
201
219
|
className: clsx('byline-analytics-stat', dashboard_module.stat, TONE_TILE[tone]),
|
|
@@ -215,22 +233,21 @@ function StatTile({ tone, label, value, foot }) {
|
|
|
215
233
|
]
|
|
216
234
|
});
|
|
217
235
|
}
|
|
218
|
-
function
|
|
236
|
+
function toRankedRow(row) {
|
|
237
|
+
const key = 'path' in row ? row.path : row.referrerHost;
|
|
219
238
|
return {
|
|
220
|
-
key
|
|
221
|
-
label:
|
|
239
|
+
key,
|
|
240
|
+
label: key,
|
|
222
241
|
value: row.views,
|
|
223
242
|
visitors: row.visitors,
|
|
224
|
-
overflow:
|
|
243
|
+
overflow: key === ANALYTICS_OVERFLOW_KEY
|
|
225
244
|
};
|
|
226
245
|
}
|
|
227
|
-
function RankedList({ title, caption, rows, tone, locale, total, coverageFrom }) {
|
|
246
|
+
function RankedList({ title, caption, rows, tone, locale, total, fullList, coverageFrom }) {
|
|
228
247
|
const { t } = useTranslation('byline-admin');
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const ceiling = Math.max(1, ...rows.map((row)=>row.value));
|
|
233
|
-
const truncated = null != total && total > rows.length;
|
|
248
|
+
const modal = useModal();
|
|
249
|
+
const preview = rows.slice(0, ANALYTICS_PREVIEW_ROWS);
|
|
250
|
+
const truncated = total > preview.length;
|
|
234
251
|
const coverageDate = useMemo(()=>new Intl.DateTimeFormat(locale, {
|
|
235
252
|
dateStyle: 'medium',
|
|
236
253
|
timeZone: 'UTC'
|
|
@@ -239,7 +256,7 @@ function RankedList({ title, caption, rows, tone, locale, total, coverageFrom })
|
|
|
239
256
|
]);
|
|
240
257
|
const description = [
|
|
241
258
|
truncated ? t('analytics.topOf', {
|
|
242
|
-
shown:
|
|
259
|
+
shown: preview.length,
|
|
243
260
|
total
|
|
244
261
|
}) : caption,
|
|
245
262
|
null == coverageFrom ? void 0 : t('analytics.coverage.since', {
|
|
@@ -250,51 +267,52 @@ function RankedList({ title, caption, rows, tone, locale, total, coverageFrom })
|
|
|
250
267
|
className: clsx('byline-analytics-list', dashboard_module.list),
|
|
251
268
|
children: [
|
|
252
269
|
/*#__PURE__*/ jsxs(Card.Header, {
|
|
270
|
+
className: clsx('byline-analytics-list-header', dashboard_module.listHeader),
|
|
253
271
|
children: [
|
|
254
|
-
/*#__PURE__*/
|
|
255
|
-
|
|
272
|
+
/*#__PURE__*/ jsxs("div", {
|
|
273
|
+
className: dashboard_module.listHeading,
|
|
274
|
+
children: [
|
|
275
|
+
/*#__PURE__*/ jsx(Card.Title, {
|
|
276
|
+
children: title
|
|
277
|
+
}),
|
|
278
|
+
description.length > 0 && /*#__PURE__*/ jsx(Card.Description, {
|
|
279
|
+
children: description
|
|
280
|
+
})
|
|
281
|
+
]
|
|
256
282
|
}),
|
|
257
|
-
|
|
258
|
-
|
|
283
|
+
null != fullList && truncated && /*#__PURE__*/ jsx(Button, {
|
|
284
|
+
type: "button",
|
|
285
|
+
size: "xs",
|
|
286
|
+
variant: "outlined",
|
|
287
|
+
className: clsx('byline-analytics-view-all', dashboard_module.viewAll),
|
|
288
|
+
onClick: modal.onOpen,
|
|
289
|
+
children: t('analytics.viewAll', {
|
|
290
|
+
count: total
|
|
291
|
+
})
|
|
259
292
|
})
|
|
260
293
|
]
|
|
261
294
|
}),
|
|
262
295
|
/*#__PURE__*/ jsx(Card.Content, {
|
|
263
|
-
children: 0 ===
|
|
296
|
+
children: 0 === preview.length ? /*#__PURE__*/ jsx("p", {
|
|
264
297
|
className: "muted",
|
|
265
298
|
children: t('analytics.empty')
|
|
266
|
-
}) : /*#__PURE__*/ jsx(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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))
|
|
299
|
+
}) : /*#__PURE__*/ jsx(RankingRows, {
|
|
300
|
+
rows: preview,
|
|
301
|
+
tone: tone,
|
|
302
|
+
locale: locale
|
|
289
303
|
})
|
|
304
|
+
}),
|
|
305
|
+
null != fullList && /*#__PURE__*/ jsx(RankedListModal, {
|
|
306
|
+
isOpen: modal.isOpen,
|
|
307
|
+
onDismiss: modal.onDismiss,
|
|
308
|
+
title: title,
|
|
309
|
+
tone: tone,
|
|
310
|
+
locale: locale,
|
|
311
|
+
source: fullList
|
|
290
312
|
})
|
|
291
313
|
]
|
|
292
314
|
});
|
|
293
315
|
}
|
|
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
316
|
function formatShare(part, whole, locale) {
|
|
299
317
|
const percent = new Intl.NumberFormat(locale, {
|
|
300
318
|
style: 'percent',
|
|
@@ -305,4 +323,4 @@ function formatShare(part, whole, locale) {
|
|
|
305
323
|
function partialCoverageFrom(rangeFrom, coverageFrom) {
|
|
306
324
|
return null != coverageFrom && coverageFrom > rangeFrom ? coverageFrom : void 0;
|
|
307
325
|
}
|
|
308
|
-
export { AnalyticsDashboard, formatShare, partialCoverageFrom, shareWidth };
|
|
326
|
+
export { ANALYTICS_PREVIEW_ROWS, AnalyticsDashboard, formatShare, partialCoverageFrom, shareWidth };
|
|
@@ -24,6 +24,15 @@ const dashboard_module = {
|
|
|
24
24
|
rankingOverflow: "rankingOverflow-hUQMKU",
|
|
25
25
|
rankingLabel: "rankingLabel-wsuniC",
|
|
26
26
|
rankingValue: "rankingValue-i_vmgf",
|
|
27
|
-
rankingVisitors: "rankingVisitors-NZl_jj"
|
|
27
|
+
rankingVisitors: "rankingVisitors-NZl_jj",
|
|
28
|
+
listHeader: "listHeader-nWIpzD",
|
|
29
|
+
listHeading: "listHeading-ljSubR",
|
|
30
|
+
viewAll: "viewAll-jPtoC0",
|
|
31
|
+
modal: "modal-_Fpti3",
|
|
32
|
+
modalCaption: "modalCaption-Z7Qsck",
|
|
33
|
+
modalContent: "modalContent-raK5q1",
|
|
34
|
+
modalStatus: "modalStatus-bXbZr9",
|
|
35
|
+
modalActions: "modalActions-XVBrOS",
|
|
36
|
+
srOnly: "srOnly-szSAhX"
|
|
28
37
|
};
|
|
29
38
|
export default dashboard_module;
|
|
@@ -266,3 +266,60 @@
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
:is(.listHeader-nWIpzD, .byline-analytics-list-header) {
|
|
270
|
+
justify-content: space-between;
|
|
271
|
+
align-items: flex-start;
|
|
272
|
+
gap: .75rem;
|
|
273
|
+
display: flex;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.listHeading-ljSubR {
|
|
277
|
+
min-width: 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
:is(.viewAll-jPtoC0, .byline-analytics-view-all) {
|
|
281
|
+
white-space: nowrap;
|
|
282
|
+
flex-shrink: 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
:is(.modal-_Fpti3, .byline-analytics-modal) {
|
|
286
|
+
width: min(40rem, 100vw - 2rem);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
:is(.modalCaption-Z7Qsck, .byline-analytics-modal-caption) {
|
|
290
|
+
font-variant-numeric: tabular-nums;
|
|
291
|
+
margin: .25rem 0 0;
|
|
292
|
+
font-size: .75rem;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
:is(.modalContent-raK5q1, .byline-analytics-modal-content) {
|
|
296
|
+
max-height: min(60vh, 48rem);
|
|
297
|
+
overflow-y: auto;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.modalStatus-bXbZr9 {
|
|
301
|
+
justify-content: center;
|
|
302
|
+
padding: 2rem 0;
|
|
303
|
+
display: flex;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
:is(.modalActions-XVBrOS, .byline-analytics-modal-actions) {
|
|
307
|
+
flex-wrap: wrap;
|
|
308
|
+
justify-content: space-between;
|
|
309
|
+
align-items: center;
|
|
310
|
+
gap: .75rem;
|
|
311
|
+
display: flex;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.srOnly-szSAhX {
|
|
315
|
+
clip: rect(0, 0, 0, 0);
|
|
316
|
+
white-space: nowrap;
|
|
317
|
+
border: 0;
|
|
318
|
+
width: 1px;
|
|
319
|
+
height: 1px;
|
|
320
|
+
margin: -1px;
|
|
321
|
+
padding: 0;
|
|
322
|
+
position: absolute;
|
|
323
|
+
overflow: hidden;
|
|
324
|
+
}
|
|
325
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Full ranking for one dashboard card, paged client-side.
|
|
10
|
+
*
|
|
11
|
+
* The dashboard cards show a short preview so the lists band stays even
|
|
12
|
+
* regardless of traffic. This modal shows the whole set behind a card's
|
|
13
|
+
* "View all" action. A `source` may be a ready list (countries are fully
|
|
14
|
+
* loaded with the dashboard) or a loader invoked once each time the modal
|
|
15
|
+
* opens (pages, downloads and referrers fetch a larger top-N on demand).
|
|
16
|
+
*
|
|
17
|
+
* The caption keeps the dashboard's honesty rule: when the loaded rows are
|
|
18
|
+
* a top-N slice of a larger set it says so alongside the page range.
|
|
19
|
+
*/
|
|
20
|
+
import type React from 'react';
|
|
21
|
+
import { type AnalyticsTone, type RankedListSource } from './ranking.js';
|
|
22
|
+
export type RankedListSourceInput = RankedListSource | (() => Promise<RankedListSource>);
|
|
23
|
+
export interface RankedListModalProps {
|
|
24
|
+
isOpen: boolean;
|
|
25
|
+
onDismiss(): void;
|
|
26
|
+
title: string;
|
|
27
|
+
tone: AnalyticsTone;
|
|
28
|
+
locale: string;
|
|
29
|
+
source: RankedListSourceInput;
|
|
30
|
+
pageSize?: number;
|
|
31
|
+
}
|
|
32
|
+
export declare function RankedListModal({ isOpen, onDismiss, title, tone, locale, source, pageSize, }: RankedListModalProps): React.JSX.Element;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useTranslation } from "@byline/i18n/react";
|
|
5
|
+
import { Alert, Button, LoaderRing, Modal, Pagination } from "@byline/ui/react";
|
|
6
|
+
import clsx from "clsx";
|
|
7
|
+
import dashboard_module from "./dashboard.module.js";
|
|
8
|
+
import { RankingRows, pageWindow } from "./ranking.js";
|
|
9
|
+
function RankedListModal({ isOpen, onDismiss, title, tone, locale, source, pageSize = 25 }) {
|
|
10
|
+
const { t } = useTranslation('byline-admin');
|
|
11
|
+
const [state, setState] = useState({
|
|
12
|
+
status: 'idle'
|
|
13
|
+
});
|
|
14
|
+
const [page, setPage] = useState(1);
|
|
15
|
+
useEffect(()=>{
|
|
16
|
+
if (!isOpen) {
|
|
17
|
+
setState({
|
|
18
|
+
status: 'idle'
|
|
19
|
+
});
|
|
20
|
+
setPage(1);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if ('function' != typeof source) return void setState({
|
|
24
|
+
status: 'ready',
|
|
25
|
+
data: source
|
|
26
|
+
});
|
|
27
|
+
let current = true;
|
|
28
|
+
setState({
|
|
29
|
+
status: 'loading'
|
|
30
|
+
});
|
|
31
|
+
source().then((data)=>{
|
|
32
|
+
if (current) setState({
|
|
33
|
+
status: 'ready',
|
|
34
|
+
data
|
|
35
|
+
});
|
|
36
|
+
}).catch(()=>{
|
|
37
|
+
if (current) setState({
|
|
38
|
+
status: 'error'
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
return ()=>{
|
|
42
|
+
current = false;
|
|
43
|
+
};
|
|
44
|
+
}, [
|
|
45
|
+
isOpen,
|
|
46
|
+
source
|
|
47
|
+
]);
|
|
48
|
+
const data = 'ready' === state.status ? state.data : void 0;
|
|
49
|
+
const loaded = data?.rows.length ?? 0;
|
|
50
|
+
const window = pageWindow(loaded, pageSize, page);
|
|
51
|
+
const visible = data?.rows.slice(window.start, window.end) ?? [];
|
|
52
|
+
const truncated = null != data && data.total > loaded;
|
|
53
|
+
const caption = data ? [
|
|
54
|
+
0 === loaded ? void 0 : t('analytics.range', {
|
|
55
|
+
from: window.start + 1,
|
|
56
|
+
to: window.end,
|
|
57
|
+
total: loaded
|
|
58
|
+
}),
|
|
59
|
+
truncated ? t('analytics.topOf', {
|
|
60
|
+
shown: loaded,
|
|
61
|
+
total: data.total
|
|
62
|
+
}) : void 0
|
|
63
|
+
].filter((value)=>null != value).join(' · ') : void 0;
|
|
64
|
+
return /*#__PURE__*/ jsx(Modal, {
|
|
65
|
+
isOpen: isOpen,
|
|
66
|
+
onDismiss: onDismiss,
|
|
67
|
+
closeOnOverlayClick: true,
|
|
68
|
+
children: /*#__PURE__*/ jsxs(Modal.Container, {
|
|
69
|
+
className: clsx('byline-analytics-modal', dashboard_module.modal),
|
|
70
|
+
children: [
|
|
71
|
+
/*#__PURE__*/ jsxs(Modal.Header, {
|
|
72
|
+
children: [
|
|
73
|
+
/*#__PURE__*/ jsx("h2", {
|
|
74
|
+
children: title
|
|
75
|
+
}),
|
|
76
|
+
null != caption && caption.length > 0 && /*#__PURE__*/ jsx("p", {
|
|
77
|
+
className: clsx('muted', 'byline-analytics-modal-caption', dashboard_module.modalCaption),
|
|
78
|
+
children: caption
|
|
79
|
+
})
|
|
80
|
+
]
|
|
81
|
+
}),
|
|
82
|
+
/*#__PURE__*/ jsxs(Modal.Content, {
|
|
83
|
+
className: clsx('byline-analytics-modal-content', dashboard_module.modalContent),
|
|
84
|
+
children: [
|
|
85
|
+
'loading' === state.status && /*#__PURE__*/ jsxs("div", {
|
|
86
|
+
role: "status",
|
|
87
|
+
"aria-live": "polite",
|
|
88
|
+
className: dashboard_module.modalStatus,
|
|
89
|
+
children: [
|
|
90
|
+
/*#__PURE__*/ jsx(LoaderRing, {
|
|
91
|
+
size: 28,
|
|
92
|
+
"aria-hidden": "true"
|
|
93
|
+
}),
|
|
94
|
+
/*#__PURE__*/ jsx("span", {
|
|
95
|
+
className: dashboard_module.srOnly,
|
|
96
|
+
children: t('common.loading')
|
|
97
|
+
})
|
|
98
|
+
]
|
|
99
|
+
}),
|
|
100
|
+
'error' === state.status && /*#__PURE__*/ jsx("div", {
|
|
101
|
+
role: "alert",
|
|
102
|
+
children: /*#__PURE__*/ jsx(Alert, {
|
|
103
|
+
intent: "danger",
|
|
104
|
+
close: false,
|
|
105
|
+
children: t('analytics.loadError')
|
|
106
|
+
})
|
|
107
|
+
}),
|
|
108
|
+
null != data && 0 === loaded && /*#__PURE__*/ jsx("p", {
|
|
109
|
+
className: "muted",
|
|
110
|
+
children: t('analytics.empty')
|
|
111
|
+
}),
|
|
112
|
+
null != data && loaded > 0 && /*#__PURE__*/ jsx(RankingRows, {
|
|
113
|
+
rows: visible,
|
|
114
|
+
tone: tone,
|
|
115
|
+
locale: locale
|
|
116
|
+
})
|
|
117
|
+
]
|
|
118
|
+
}),
|
|
119
|
+
/*#__PURE__*/ jsxs(Modal.Actions, {
|
|
120
|
+
className: clsx('byline-analytics-modal-actions', dashboard_module.modalActions),
|
|
121
|
+
children: [
|
|
122
|
+
window.pageCount > 1 ? /*#__PURE__*/ jsx(Pagination, {
|
|
123
|
+
variant: "dashboard",
|
|
124
|
+
count: window.pageCount,
|
|
125
|
+
page: Math.min(page, window.pageCount),
|
|
126
|
+
onChange: (_event, next)=>setPage(next),
|
|
127
|
+
children: /*#__PURE__*/ jsx(Pagination.Root, {
|
|
128
|
+
ariaLabel: t('analytics.pager'),
|
|
129
|
+
children: /*#__PURE__*/ jsx(Pagination.Pager, {})
|
|
130
|
+
})
|
|
131
|
+
}) : /*#__PURE__*/ jsx("span", {}),
|
|
132
|
+
/*#__PURE__*/ jsx(Button, {
|
|
133
|
+
type: "button",
|
|
134
|
+
size: "sm",
|
|
135
|
+
variant: "outlined",
|
|
136
|
+
onClick: onDismiss,
|
|
137
|
+
children: t('common.actions.close')
|
|
138
|
+
})
|
|
139
|
+
]
|
|
140
|
+
})
|
|
141
|
+
]
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
export { RankedListModal };
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Ranked-row rendering shared by the dashboard cards and the full-list
|
|
10
|
+
* modal, plus the small pure helpers both rely on.
|
|
11
|
+
*/
|
|
12
|
+
import type React from 'react';
|
|
13
|
+
export type AnalyticsTone = 'views' | 'visitors' | 'downloads';
|
|
14
|
+
export interface RankedRow {
|
|
15
|
+
key: string;
|
|
16
|
+
label: string;
|
|
17
|
+
value: number;
|
|
18
|
+
visitors: number;
|
|
19
|
+
overflow: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** A complete ranking plus the size of the set it was drawn from. */
|
|
22
|
+
export interface RankedListSource {
|
|
23
|
+
rows: RankedRow[];
|
|
24
|
+
total: number;
|
|
25
|
+
}
|
|
26
|
+
/** The ordered list with a share bar behind each row. Renders no card chrome. */
|
|
27
|
+
export declare function RankingRows({ rows, tone, locale, }: {
|
|
28
|
+
rows: RankedRow[];
|
|
29
|
+
tone: AnalyticsTone;
|
|
30
|
+
locale: string;
|
|
31
|
+
}): React.JSX.Element;
|
|
32
|
+
/** Never collapse the bar entirely: a visible sliver still encodes "smallest". */
|
|
33
|
+
export declare function shareWidth(value: number, ceiling: number): number;
|
|
34
|
+
/** Row indices for one page of a list, with the page clamped into range. */
|
|
35
|
+
export declare function pageWindow(total: number, pageSize: number, page: number): {
|
|
36
|
+
start: number;
|
|
37
|
+
end: number;
|
|
38
|
+
pageCount: number;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Localised region name for an ISO 3166-1 alpha-2 code, or the code itself
|
|
42
|
+
* when the value is not a code or the runtime has no name for it. The
|
|
43
|
+
* dashboard stores bare codes, so this is purely a display concern.
|
|
44
|
+
*/
|
|
45
|
+
export declare function regionName(code: string, locale: string): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { useTranslation } from "@byline/i18n/react";
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
import dashboard_module from "./dashboard.module.js";
|
|
7
|
+
const TONE_BAR = {
|
|
8
|
+
views: dashboard_module.barViews,
|
|
9
|
+
visitors: dashboard_module.barVisitors,
|
|
10
|
+
downloads: dashboard_module.barDownloads
|
|
11
|
+
};
|
|
12
|
+
function RankingRows({ rows, tone, locale }) {
|
|
13
|
+
const { t } = useTranslation('byline-admin');
|
|
14
|
+
const numbers = useMemo(()=>new Intl.NumberFormat(locale), [
|
|
15
|
+
locale
|
|
16
|
+
]);
|
|
17
|
+
const ceiling = Math.max(1, ...rows.map((row)=>row.value));
|
|
18
|
+
return /*#__PURE__*/ jsx("ol", {
|
|
19
|
+
className: clsx('byline-analytics-ranking', dashboard_module.ranking),
|
|
20
|
+
children: rows.map((row)=>/*#__PURE__*/ jsxs("li", {
|
|
21
|
+
className: clsx('byline-analytics-ranking-row', dashboard_module.rankingRow, TONE_BAR[tone], row.overflow && dashboard_module.rankingOverflow),
|
|
22
|
+
style: {
|
|
23
|
+
'--byline-analytics-share': `${shareWidth(row.value, ceiling)}%`
|
|
24
|
+
},
|
|
25
|
+
children: [
|
|
26
|
+
/*#__PURE__*/ jsx("span", {
|
|
27
|
+
className: dashboard_module.rankingLabel,
|
|
28
|
+
title: row.key,
|
|
29
|
+
children: row.overflow ? t('analytics.overflow') : row.label
|
|
30
|
+
}),
|
|
31
|
+
/*#__PURE__*/ jsx("span", {
|
|
32
|
+
className: dashboard_module.rankingValue,
|
|
33
|
+
children: numbers.format(row.value)
|
|
34
|
+
}),
|
|
35
|
+
/*#__PURE__*/ jsx("span", {
|
|
36
|
+
className: dashboard_module.rankingVisitors,
|
|
37
|
+
children: numbers.format(row.visitors)
|
|
38
|
+
})
|
|
39
|
+
]
|
|
40
|
+
}, row.key))
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function shareWidth(value, ceiling) {
|
|
44
|
+
if (!Number.isFinite(value) || value <= 0 || ceiling <= 0) return 0;
|
|
45
|
+
return Math.max(3, Math.min(100, value / ceiling * 100));
|
|
46
|
+
}
|
|
47
|
+
function pageWindow(total, pageSize, page) {
|
|
48
|
+
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
49
|
+
const current = Math.min(Math.max(1, page), pageCount);
|
|
50
|
+
const start = (current - 1) * pageSize;
|
|
51
|
+
return {
|
|
52
|
+
start,
|
|
53
|
+
end: Math.min(total, start + pageSize),
|
|
54
|
+
pageCount
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function regionName(code, locale) {
|
|
58
|
+
if (!/^[A-Za-z]{2}$/.test(code)) return code;
|
|
59
|
+
try {
|
|
60
|
+
const names = new Intl.DisplayNames([
|
|
61
|
+
locale
|
|
62
|
+
], {
|
|
63
|
+
type: 'region',
|
|
64
|
+
fallback: 'none'
|
|
65
|
+
});
|
|
66
|
+
return names.of(code.toUpperCase()) ?? code;
|
|
67
|
+
} catch {
|
|
68
|
+
return code;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export { RankingRows, pageWindow, regionName, shareWidth };
|
|
@@ -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 {};
|