hazo_umetrics 1.6.2 → 1.6.3
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/api/index.d.ts.map +1 -1
- package/dist/api/index.js +3 -1
- package/dist/server/behavior.d.ts +15 -2
- package/dist/server/behavior.d.ts.map +1 -1
- package/dist/server/behavior.js +51 -15
- package/dist/ui/journey_panel.d.ts +12 -3
- package/dist/ui/journey_panel.d.ts.map +1 -1
- package/dist/ui/journey_panel.js +140 -48
- package/package.json +1 -1
package/dist/api/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"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"}
|
package/dist/api/index.js
CHANGED
|
@@ -859,6 +859,8 @@ export function createBehaviorHandlers(options) {
|
|
|
859
859
|
const user_id = url.searchParams.get('user_id') ?? undefined;
|
|
860
860
|
const session_id = url.searchParams.get('session_id') ?? undefined;
|
|
861
861
|
const since = url.searchParams.get('since') ?? undefined;
|
|
862
|
+
const splitByPage = url.searchParams.get('split_by_page') === '1';
|
|
863
|
+
const pageFlow = url.searchParams.get('page_flow') === '1';
|
|
862
864
|
const gate = await gateAuth(req, {
|
|
863
865
|
required_permissions: ['metrics.view'],
|
|
864
866
|
app_id: app_id || undefined,
|
|
@@ -871,7 +873,7 @@ export function createBehaviorHandlers(options) {
|
|
|
871
873
|
const adapter = await options.getAdapter();
|
|
872
874
|
const catalog = await _listActions(adapter, { app_id });
|
|
873
875
|
const catalogMap = new Map(catalog.map((a) => [a.action_key, { label: a.label, datapoint_schema: a.datapoint_schema }]));
|
|
874
|
-
const result = await _userJourney(adapter, { app_id, user_id, session_id, since, catalogMap });
|
|
876
|
+
const result = await _userJourney(adapter, { app_id, user_id, session_id, since, catalogMap, splitByPage, pageFlow });
|
|
875
877
|
return jsonOk(result);
|
|
876
878
|
},
|
|
877
879
|
};
|
|
@@ -63,7 +63,7 @@ export declare function topActions(adapter: HazoConnectAdapter, opts: BehaviorOp
|
|
|
63
63
|
export declare function eventsOverTime(adapter: HazoConnectAdapter, opts: BehaviorOptions): Promise<EventDayBucket[]>;
|
|
64
64
|
export interface JourneyStep {
|
|
65
65
|
metric_key: string;
|
|
66
|
-
kind: 'action' | 'pageview' | 'error' | 'other';
|
|
66
|
+
kind: 'action' | 'pageview' | 'click' | 'error' | 'other';
|
|
67
67
|
label: string;
|
|
68
68
|
path?: string;
|
|
69
69
|
captured_at: string;
|
|
@@ -81,8 +81,14 @@ export interface JourneySession {
|
|
|
81
81
|
export interface JourneyGraphNode {
|
|
82
82
|
id: string;
|
|
83
83
|
label: string;
|
|
84
|
-
kind: 'action' | 'pageview' | 'error' | 'other';
|
|
84
|
+
kind: 'action' | 'pageview' | 'click' | 'error' | 'other';
|
|
85
85
|
count: number;
|
|
86
|
+
/** Pages this event fired on, most frequent first. Lets the UI answer
|
|
87
|
+
* "which page is this click from?" for nodes keyed only by metric_key. */
|
|
88
|
+
paths?: Array<{
|
|
89
|
+
path: string;
|
|
90
|
+
count: number;
|
|
91
|
+
}>;
|
|
86
92
|
}
|
|
87
93
|
export interface JourneyGraphEdge {
|
|
88
94
|
from: string;
|
|
@@ -108,6 +114,13 @@ export declare function userJourney(adapter: HazoConnectAdapter, opts: {
|
|
|
108
114
|
label?: string;
|
|
109
115
|
datapoint_schema?: DatapointSchema;
|
|
110
116
|
}>;
|
|
117
|
+
/** When true, nodes/edges are keyed by (metric_key + page) so the same event
|
|
118
|
+
* on different pages becomes distinct boxes. Removes cross-page ambiguity. */
|
|
119
|
+
splitByPage?: boolean;
|
|
120
|
+
/** When true, only pageview events form the graph and each pageview links to
|
|
121
|
+
* the NEXT pageview in the session (intervening clicks are skipped), giving a
|
|
122
|
+
* connected page-to-page navigation flow. */
|
|
123
|
+
pageFlow?: boolean;
|
|
111
124
|
}): Promise<JourneyResult>;
|
|
112
125
|
export interface DatapointKeyStats {
|
|
113
126
|
key: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"behavior.d.ts","sourceRoot":"","sources":["../../src/server/behavior.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAsB7D,MAAM,MAAM,WAAW,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AACzE,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAC3D,MAAM,MAAM,cAAc,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAM7F,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACpC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAqED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAgCxB;AAMD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAkDxB;AAMD;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GACvD,OAAO,CAAC,WAAW,EAAE,CAAC,CAmCxB;AAMD;;GAEG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAexB;AAMD;;GAEG;AACH,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,UAAU,EAAE,CAAC,CAmBvB;AAMD;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CA8BxB;AAMD;;GAEG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAgBxB;AAMD;;;GAGG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,cAAc,EAAE,CAAC,CAoC3B;AAMD,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"behavior.d.ts","sourceRoot":"","sources":["../../src/server/behavior.ts"],"names":[],"mappings":"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"}
|
package/dist/server/behavior.js
CHANGED
|
@@ -347,8 +347,11 @@ export async function eventsOverTime(adapter, opts) {
|
|
|
347
347
|
function classifyKind(metric_key) {
|
|
348
348
|
if (metric_key.startsWith('action.'))
|
|
349
349
|
return 'action';
|
|
350
|
-
|
|
350
|
+
// Pageview keys are emitted as `ui.pageview.*` (also accept bare `pageview*`).
|
|
351
|
+
if (metric_key.startsWith('ui.pageview') || metric_key.startsWith('pageview'))
|
|
351
352
|
return 'pageview';
|
|
353
|
+
if (metric_key.startsWith('ui.click.'))
|
|
354
|
+
return 'click';
|
|
352
355
|
if (metric_key.startsWith('ui.error.') || metric_key.startsWith('error.'))
|
|
353
356
|
return 'error';
|
|
354
357
|
return 'other';
|
|
@@ -386,9 +389,22 @@ export async function userJourney(adapter, opts) {
|
|
|
386
389
|
const nodeCountMap = new Map();
|
|
387
390
|
const edgeAccMap = new Map();
|
|
388
391
|
const sessions = [];
|
|
392
|
+
// Node/edge identity. In split-by-page mode the same event on two pages gets
|
|
393
|
+
// two distinct ids (NUL separator — never appears in a metric_key or path).
|
|
394
|
+
const scopedId = (mk, d) => {
|
|
395
|
+
if (!opts.splitByPage)
|
|
396
|
+
return mk;
|
|
397
|
+
const p = d.path;
|
|
398
|
+
return p ? `${mk}@@${p}` : mk;
|
|
399
|
+
};
|
|
389
400
|
for (const [sessionKey, sessionRows] of sessionGroups) {
|
|
390
401
|
// fetchWindow returns rows in ascending captured_at order
|
|
391
402
|
const steps = [];
|
|
403
|
+
// Last node accumulated into the graph for this session. Edges connect it to
|
|
404
|
+
// the next accumulated node — so in page-flow mode (where non-pageviews are
|
|
405
|
+
// skipped) a pageview links to the next pageview, not the intervening click.
|
|
406
|
+
let prevNodeId = null;
|
|
407
|
+
let prevCapturedMs = null;
|
|
392
408
|
for (let i = 0; i < sessionRows.length; i++) {
|
|
393
409
|
const row = sessionRows[i];
|
|
394
410
|
const dims = parseDims(row.dimensions);
|
|
@@ -397,7 +413,11 @@ export async function userJourney(adapter, opts) {
|
|
|
397
413
|
// Look up catalog entry by action_key (strips 'action.' prefix)
|
|
398
414
|
const actionKey = metric_key.startsWith('action.') ? metric_key.slice('action.'.length) : metric_key;
|
|
399
415
|
const catalogEntry = opts.catalogMap?.get(actionKey);
|
|
400
|
-
|
|
416
|
+
// Prefer a human label: curated catalog label → click text captured in
|
|
417
|
+
// dims.label → page path (for pageviews) → raw metric_key as last resort.
|
|
418
|
+
const dimLabel = typeof dims.label === 'string' && dims.label.trim() ? dims.label.trim() : undefined;
|
|
419
|
+
const prettyFallback = kind === 'pageview' ? (dims.path ?? metric_key) : metric_key;
|
|
420
|
+
const label = catalogEntry?.label ?? dimLabel ?? prettyFallback;
|
|
401
421
|
// Duration to next step in this session
|
|
402
422
|
const nextRow = sessionRows[i + 1];
|
|
403
423
|
let durationMsToNext = null;
|
|
@@ -428,24 +448,32 @@ export async function userJourney(adapter, opts) {
|
|
|
428
448
|
session_id: dims.session_id ?? '__anon__',
|
|
429
449
|
user_id: dims.user_id,
|
|
430
450
|
});
|
|
431
|
-
//
|
|
432
|
-
if (
|
|
433
|
-
|
|
451
|
+
// In page-flow mode, only pageviews participate in the graph.
|
|
452
|
+
if (opts.pageFlow && kind !== 'pageview')
|
|
453
|
+
continue;
|
|
454
|
+
// Accumulate node count + per-page tally (keyed by scoped id)
|
|
455
|
+
const nodeId = scopedId(metric_key, dims);
|
|
456
|
+
if (!nodeCountMap.has(nodeId)) {
|
|
457
|
+
nodeCountMap.set(nodeId, { label, kind, count: 0, pathCounts: new Map() });
|
|
434
458
|
}
|
|
435
|
-
nodeCountMap.get(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
459
|
+
const nodeAcc = nodeCountMap.get(nodeId);
|
|
460
|
+
nodeAcc.count++;
|
|
461
|
+
const stepPath = dims.path;
|
|
462
|
+
if (stepPath)
|
|
463
|
+
nodeAcc.pathCounts.set(stepPath, (nodeAcc.pathCounts.get(stepPath) ?? 0) + 1);
|
|
464
|
+
// Edge from the previous accumulated node to this one.
|
|
465
|
+
const nowMs = new Date(row.captured_at).getTime();
|
|
466
|
+
if (prevNodeId !== null) {
|
|
467
|
+
const edgeKey = `${prevNodeId}→${nodeId}`;
|
|
440
468
|
if (!edgeAccMap.has(edgeKey)) {
|
|
441
|
-
edgeAccMap.set(edgeKey, { from:
|
|
469
|
+
edgeAccMap.set(edgeKey, { from: prevNodeId, to: nodeId, count: 0, totalMs: 0 });
|
|
442
470
|
}
|
|
443
471
|
const edgeData = edgeAccMap.get(edgeKey);
|
|
444
472
|
edgeData.count++;
|
|
445
|
-
|
|
446
|
-
const t2 = new Date(row.captured_at).getTime();
|
|
447
|
-
edgeData.totalMs += (t2 - t1);
|
|
473
|
+
edgeData.totalMs += prevCapturedMs !== null ? (nowMs - prevCapturedMs) : 0;
|
|
448
474
|
}
|
|
475
|
+
prevNodeId = nodeId;
|
|
476
|
+
prevCapturedMs = nowMs;
|
|
449
477
|
}
|
|
450
478
|
sessions.push({
|
|
451
479
|
session_id: sessionKey,
|
|
@@ -455,7 +483,15 @@ export async function userJourney(adapter, opts) {
|
|
|
455
483
|
}
|
|
456
484
|
// Build sorted graph
|
|
457
485
|
const nodes = [...nodeCountMap.entries()]
|
|
458
|
-
.map(([id, data]) => ({
|
|
486
|
+
.map(([id, data]) => ({
|
|
487
|
+
id,
|
|
488
|
+
label: data.label,
|
|
489
|
+
kind: data.kind,
|
|
490
|
+
count: data.count,
|
|
491
|
+
paths: [...data.pathCounts.entries()]
|
|
492
|
+
.map(([path, count]) => ({ path, count }))
|
|
493
|
+
.sort((a, b) => b.count - a.count),
|
|
494
|
+
}))
|
|
459
495
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
460
496
|
const edges = [...edgeAccMap.values()]
|
|
461
497
|
.map(({ from, to, count, totalMs }) => ({ from, to, count, avgMs: count > 0 ? totalMs / count : 0 }))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export interface JourneyStep {
|
|
2
2
|
metric_key: string;
|
|
3
|
-
kind: 'action' | 'pageview' | 'error' | 'other';
|
|
3
|
+
kind: 'action' | 'pageview' | 'click' | 'error' | 'other';
|
|
4
4
|
label: string;
|
|
5
5
|
path?: string;
|
|
6
6
|
captured_at: string;
|
|
@@ -18,8 +18,13 @@ export interface JourneySession {
|
|
|
18
18
|
export interface JourneyGraphNode {
|
|
19
19
|
id: string;
|
|
20
20
|
label: string;
|
|
21
|
-
kind: 'action' | 'pageview' | 'error' | 'other';
|
|
21
|
+
kind: 'action' | 'pageview' | 'click' | 'error' | 'other';
|
|
22
22
|
count: number;
|
|
23
|
+
/** Pages this event fired on, most frequent first. */
|
|
24
|
+
paths?: Array<{
|
|
25
|
+
path: string;
|
|
26
|
+
count: number;
|
|
27
|
+
}>;
|
|
23
28
|
}
|
|
24
29
|
export interface JourneyGraphEdge {
|
|
25
30
|
from: string;
|
|
@@ -46,6 +51,10 @@ export interface JourneyPanelProps {
|
|
|
46
51
|
name?: string;
|
|
47
52
|
profile_picture_url?: string;
|
|
48
53
|
}>>;
|
|
54
|
+
/** Let edges enter a node's top/bottom (not only its left) to reduce crossings. Default true. */
|
|
55
|
+
multiSidePorts?: boolean;
|
|
56
|
+
/** Route back edges forward out of the source before curving back. Default true. */
|
|
57
|
+
backEdgeLoop?: boolean;
|
|
49
58
|
}
|
|
50
|
-
export declare function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserProfiles }: JourneyPanelProps): import("react").JSX.Element;
|
|
59
|
+
export declare function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserProfiles, multiSidePorts, backEdgeLoop }: JourneyPanelProps): import("react").JSX.Element;
|
|
51
60
|
//# sourceMappingURL=journey_panel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journey_panel.d.ts","sourceRoot":"","sources":["../../src/ui/journey_panel.tsx"],"names":[],"mappings":"AAiBA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"journey_panel.d.ts","sourceRoot":"","sources":["../../src/ui/journey_panel.tsx"],"names":[],"mappings":"AAiBA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,KAAK,EAAE,YAAY,CAAC;CACrB;AAMD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IAC1I,iGAAiG;IACjG,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oFAAoF;IACpF,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AA4kBD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,mBAAmB,EAAE,cAAqB,EAAE,YAAmB,EAAE,EAAE,iBAAiB,+BAiLrJ"}
|
package/dist/ui/journey_panel.js
CHANGED
|
@@ -18,6 +18,7 @@ function kindColor(kind) {
|
|
|
18
18
|
switch (kind) {
|
|
19
19
|
case 'action': return '#6366f1';
|
|
20
20
|
case 'pageview': return '#10b981';
|
|
21
|
+
case 'click': return '#f59e0b';
|
|
21
22
|
case 'error': return '#ef4444';
|
|
22
23
|
default: return '#94a3b8';
|
|
23
24
|
}
|
|
@@ -27,7 +28,14 @@ function clampZoom(z) { return Math.max(0.25, Math.min(3, z)); }
|
|
|
27
28
|
// JourneyFlowGraph — SVG left-to-right DAG visualization
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
const NODE_W = 148;
|
|
30
|
-
const NODE_H =
|
|
31
|
+
const NODE_H = 56;
|
|
32
|
+
/** Shorten a page path for display inside a node (keeps the tail, which is the
|
|
33
|
+
* distinguishing part: /board-games/backgammon-timer → …/backgammon-timer). */
|
|
34
|
+
function shortPath(path, max = 20) {
|
|
35
|
+
if (path.length <= max)
|
|
36
|
+
return path;
|
|
37
|
+
return '…' + path.slice(path.length - (max - 1));
|
|
38
|
+
}
|
|
31
39
|
const COL_GAP = 200;
|
|
32
40
|
const ROW_GAP = 64;
|
|
33
41
|
const PAD = 24;
|
|
@@ -103,13 +111,75 @@ function computeFlowLayout(nodes, edges) {
|
|
|
103
111
|
}
|
|
104
112
|
return pos;
|
|
105
113
|
}
|
|
106
|
-
|
|
114
|
+
/** A node port: anchor point on a node side plus the outward unit normal. The
|
|
115
|
+
* normal drives the bezier control point so the line leaves/enters square to
|
|
116
|
+
* the side (arrowheads sit flush and curves don't clip the node corner). */
|
|
117
|
+
function portPoint(x, y, side) {
|
|
118
|
+
switch (side) {
|
|
119
|
+
case 'left': return { px: x, py: y + NODE_H / 2, ux: -1, uy: 0 };
|
|
120
|
+
case 'right': return { px: x + NODE_W, py: y + NODE_H / 2, ux: 1, uy: 0 };
|
|
121
|
+
case 'top': return { px: x + NODE_W / 2, py: y, ux: 0, uy: -1 };
|
|
122
|
+
case 'bottom': return { px: x + NODE_W / 2, py: y + NODE_H, ux: 0, uy: 1 };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Pick exit/entry sides for an edge and emit its cubic-bezier path plus a
|
|
126
|
+
* label anchor (bezier midpoint). `sx,sy`/`tx,ty` are node top-left corners in
|
|
127
|
+
* screen space.
|
|
128
|
+
*
|
|
129
|
+
* - multiSidePorts: let steep forward edges enter the target's top/bottom, and
|
|
130
|
+
* same-column neighbours connect bottom→top, instead of always entering the
|
|
131
|
+
* left side. Cuts the fan of crossings that piles up on a single side.
|
|
132
|
+
* - backEdgeLoop: for back edges (target left of source) leave the source's
|
|
133
|
+
* right side — going forward a little — then curve back into the target's
|
|
134
|
+
* top/bottom, instead of cutting straight back across the source. */
|
|
135
|
+
function buildEdgePath(sx, sy, tx, ty, multiSidePorts, backEdgeLoop) {
|
|
136
|
+
const dx = tx - sx;
|
|
137
|
+
const dyc = ty - sy; // top-left deltas; centers share the same delta
|
|
138
|
+
const COL_TOL = (NODE_W + COL_GAP) * 0.5;
|
|
139
|
+
let exitSide = 'right';
|
|
140
|
+
let entrySide = 'left';
|
|
141
|
+
if (dx > COL_TOL) {
|
|
142
|
+
// Forward edge. Steep ones enter top/bottom so they don't crowd the left.
|
|
143
|
+
if (multiSidePorts && Math.abs(dyc) > Math.max(NODE_H * 1.5, dx * 0.8)) {
|
|
144
|
+
entrySide = dyc > 0 ? 'top' : 'bottom';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else if (dx < -COL_TOL) {
|
|
148
|
+
// Back edge: target is to the left.
|
|
149
|
+
if (backEdgeLoop) {
|
|
150
|
+
exitSide = 'right'; // hook forward first
|
|
151
|
+
entrySide = dyc >= 0 ? 'top' : 'bottom';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// Same column: vertical neighbours connect bottom↔top.
|
|
156
|
+
if (multiSidePorts || backEdgeLoop) {
|
|
157
|
+
exitSide = dyc >= 0 ? 'bottom' : 'top';
|
|
158
|
+
entrySide = dyc >= 0 ? 'top' : 'bottom';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const e = portPoint(sx, sy, exitSide);
|
|
162
|
+
const n = portPoint(tx, ty, entrySide);
|
|
163
|
+
const dist = Math.hypot(n.px - e.px, n.py - e.py);
|
|
164
|
+
const k = Math.max(40, dist * 0.5);
|
|
165
|
+
const c1x = e.px + e.ux * k, c1y = e.py + e.uy * k;
|
|
166
|
+
const c2x = n.px + n.ux * k, c2y = n.py + n.uy * k;
|
|
167
|
+
const d = `M${e.px},${e.py} C${c1x},${c1y} ${c2x},${c2y} ${n.px},${n.py}`;
|
|
168
|
+
// Cubic bezier midpoint (t=0.5): 0.125·P0 + 0.375·P1 + 0.375·P2 + 0.125·P3.
|
|
169
|
+
const lx = 0.125 * e.px + 0.375 * c1x + 0.375 * c2x + 0.125 * n.px;
|
|
170
|
+
const ly = 0.125 * e.py + 0.375 * c1y + 0.375 * c2y + 0.125 * n.py;
|
|
171
|
+
return { d, lx, ly };
|
|
172
|
+
}
|
|
173
|
+
function JourneyFlowGraph({ graph, userFilter, loading, multiSidePorts = true, backEdgeLoop = true }) {
|
|
107
174
|
const [zoom, setZoom] = useState(1.0);
|
|
108
175
|
const [pan, setPan] = useState({ x: 0, y: 0 });
|
|
109
176
|
const [dragging, setDragging] = useState(false);
|
|
110
177
|
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
|
111
178
|
const [selectedNode, setSelectedNode] = useState(null);
|
|
112
179
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
180
|
+
const [visibleKinds, setVisibleKinds] = useState({
|
|
181
|
+
action: true, pageview: true, click: true, error: true, other: true,
|
|
182
|
+
});
|
|
113
183
|
const wrapperRef = useRef(null);
|
|
114
184
|
const containerRef = useRef(null);
|
|
115
185
|
// Holds latest fitToScreen so the fullscreenchange effect can call it without
|
|
@@ -150,7 +220,11 @@ function JourneyFlowGraph({ graph, userFilter }) {
|
|
|
150
220
|
}, []);
|
|
151
221
|
if (graph.nodes.length === 0)
|
|
152
222
|
return null;
|
|
153
|
-
|
|
223
|
+
// Apply kind filters: drop hidden-kind nodes and any edge touching them.
|
|
224
|
+
const vNodes = graph.nodes.filter(n => visibleKinds[n.kind]);
|
|
225
|
+
const vIds = new Set(vNodes.map(n => n.id));
|
|
226
|
+
const vEdges = graph.edges.filter(e => vIds.has(e.from) && vIds.has(e.to));
|
|
227
|
+
const pos = computeFlowLayout(vNodes, vEdges);
|
|
154
228
|
// Compute SVG dimensions
|
|
155
229
|
let maxX = 0;
|
|
156
230
|
let maxY = 0;
|
|
@@ -192,7 +266,7 @@ function JourneyFlowGraph({ graph, userFilter }) {
|
|
|
192
266
|
const highlightedNodes = new Set();
|
|
193
267
|
if (selectedNode) {
|
|
194
268
|
highlightedNodes.add(selectedNode);
|
|
195
|
-
|
|
269
|
+
vEdges.forEach((edge, i) => {
|
|
196
270
|
if (edge.from === selectedNode || edge.to === selectedNode) {
|
|
197
271
|
highlightedEdges.add(i);
|
|
198
272
|
highlightedNodes.add(edge.from);
|
|
@@ -201,44 +275,54 @@ function JourneyFlowGraph({ graph, userFilter }) {
|
|
|
201
275
|
});
|
|
202
276
|
}
|
|
203
277
|
const hasSelection = selectedNode !== null;
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
278
|
+
const selectedNodeObj = selectedNode ? vNodes.find(n => n.id === selectedNode) ?? null : null;
|
|
279
|
+
const maxCount = Math.max(...vNodes.map(n => n.count), 1);
|
|
280
|
+
return (_jsxs("div", { ref: wrapperRef, className: "rounded-lg border bg-white", style: isFullscreen ? { display: 'flex', flexDirection: 'column', height: '100vh', borderRadius: 0 } : undefined, children: [_jsxs("div", { className: "px-4 py-2 border-b flex items-center gap-2 flex-wrap", children: [_jsx("span", { className: "text-xs font-semibold text-gray-600 uppercase tracking-wide", children: "Flow Graph" }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", vNodes.length, " nodes \u00B7 ", vEdges.length, " transitions)"] }), loading && _jsx("span", { className: "text-[10px] text-indigo-400 animate-pulse", children: "updating\u2026" }), isFullscreen && userFilter && (_jsx("div", { className: "flex items-center gap-1.5 text-xs text-gray-500", children: userFilter })), _jsxs("div", { className: "ml-auto flex items-center gap-3 text-[10px] text-gray-400", children: [[
|
|
281
|
+
['pageview', '#10b981'],
|
|
282
|
+
['click', '#f59e0b'],
|
|
283
|
+
['action', '#6366f1'],
|
|
284
|
+
['error', '#ef4444'],
|
|
285
|
+
['other', '#94a3b8'],
|
|
286
|
+
].map(([kind, color]) => (_jsxs("label", { className: "flex items-center gap-1 cursor-pointer select-none", title: `Toggle ${kind} nodes`, children: [_jsx("input", { type: "checkbox", checked: visibleKinds[kind], onChange: (e) => setVisibleKinds(v => ({ ...v, [kind]: e.target.checked })), className: "w-3 h-3 cursor-pointer", style: { accentColor: color } }), kind] }, kind))), _jsxs("div", { className: "flex items-center gap-1 ml-2", children: [_jsx("button", { onClick: () => setZoom(z => clampZoom(z * 1.25)), className: "px-1.5 py-0.5 text-xs border rounded hover:bg-gray-50", title: "Zoom in", children: "+" }), _jsx("button", { onClick: () => setZoom(z => clampZoom(z / 1.25)), className: "px-1.5 py-0.5 text-xs border rounded hover:bg-gray-50", title: "Zoom out", children: "\u2212" }), _jsx("button", { onClick: fitToScreen, className: "px-1.5 py-0.5 text-xs border rounded hover:bg-gray-50", title: "Fit to screen", children: "\u22A1" }), _jsx("button", { onClick: () => setSelectedNode(null), disabled: !hasSelection, className: `px-1.5 py-0.5 text-xs border rounded flex items-center ${hasSelection ? 'bg-indigo-50 border-indigo-300 text-indigo-600 hover:bg-indigo-100' : 'text-gray-300 cursor-default'}`, title: "Clear selection", children: _jsx(MousePointer2, { className: "w-3 h-3" }) }), _jsx("button", { onClick: toggleFullscreen, className: "px-1.5 py-0.5 text-xs border rounded flex items-center hover:bg-gray-50", title: isFullscreen ? 'Exit fullscreen' : 'Fullscreen', children: isFullscreen ? _jsx(Minimize2, { className: "w-3 h-3" }) : _jsx(Maximize2, { className: "w-3 h-3" }) })] })] })] }), _jsxs("div", { ref: containerRef, style: { position: 'relative', height: isFullscreen ? undefined : '500px', flex: isFullscreen ? 1 : undefined, overflow: 'hidden', cursor: dragging ? 'grabbing' : 'grab' }, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, children: [_jsxs("svg", { width: svgW, height: Math.max(svgH, 200), style: { display: 'block', transform: `translate(${pan.x}px,${pan.y}px) scale(${zoom})`, transformOrigin: '0 0', userSelect: 'none' }, children: [_jsx("defs", { children: _jsx("marker", { id: "arrow", markerWidth: "8", markerHeight: "8", refX: "7", refY: "3", orient: "auto", children: _jsx("path", { d: "M0,0 L0,6 L8,3 z", fill: "#94a3b8" }) }) }), vEdges.map((edge, i) => {
|
|
287
|
+
const from = pos.get(edge.from);
|
|
288
|
+
const to = pos.get(edge.to);
|
|
289
|
+
if (!from || !to)
|
|
290
|
+
return null;
|
|
291
|
+
const route = buildEdgePath(from.x, from.y + offsetY, to.x, to.y + offsetY, multiSidePorts, backEdgeLoop);
|
|
292
|
+
const isHighlighted = hasSelection ? highlightedEdges.has(i) : true;
|
|
293
|
+
const edgeOpacity = isHighlighted ? Math.max(0.4, edge.count / maxCount) : 0.05;
|
|
294
|
+
const edgeStroke = isHighlighted && hasSelection ? '#6366f1' : '#94a3b8';
|
|
295
|
+
const edgeStrokeW = isHighlighted ? Math.max(1.5, Math.min(4, edge.count / maxCount * 4)) : 1;
|
|
296
|
+
return (_jsxs("g", { children: [_jsx("path", { d: route.d, fill: "none", stroke: edgeStroke, strokeWidth: edgeStrokeW, strokeOpacity: edgeOpacity, markerEnd: "url(#arrow)" }), (() => {
|
|
297
|
+
// On selection, label every connected edge (even 1×) boldly so
|
|
298
|
+
// occurrence counts are readable; otherwise keep the faint >1 hint.
|
|
299
|
+
const selectedEdge = hasSelection && isHighlighted;
|
|
300
|
+
const showLabel = hasSelection ? isHighlighted : edge.count > 1;
|
|
301
|
+
if (!showLabel)
|
|
302
|
+
return null;
|
|
303
|
+
return (_jsxs("text", { x: route.lx, y: route.ly - 4, textAnchor: "middle", fontSize: selectedEdge ? 12 : 9, fontWeight: selectedEdge ? 700 : 400, fill: selectedEdge ? '#4338ca' : '#94a3b8', opacity: selectedEdge ? 1 : edgeOpacity, stroke: selectedEdge ? '#ffffff' : undefined, strokeWidth: selectedEdge ? 3 : undefined, paintOrder: "stroke", children: [edge.count, "\u00D7"] }));
|
|
304
|
+
})()] }, i));
|
|
305
|
+
}), vNodes.map((node) => {
|
|
306
|
+
const p = pos.get(node.id);
|
|
307
|
+
if (!p)
|
|
308
|
+
return null;
|
|
309
|
+
const x = p.x;
|
|
310
|
+
const y = p.y + offsetY;
|
|
311
|
+
const fill = kindColor(node.kind);
|
|
312
|
+
const isNodeHighlighted = hasSelection ? highlightedNodes.has(node.id) : true;
|
|
313
|
+
const nodeAlpha = isNodeHighlighted ? Math.max(0.55, node.count / maxCount) : 0.1;
|
|
314
|
+
const nodeStrokeW = node.id === selectedNode ? 2.5 : 1.5;
|
|
315
|
+
const nodeStrokeOpacity = isNodeHighlighted ? Math.min(1, nodeAlpha + 0.3) : 0.1;
|
|
316
|
+
const label = node.label.length > 18 ? node.label.slice(0, 17) + '…' : node.label;
|
|
317
|
+
// Dominant page shown inline on click/action/other nodes (a pageview
|
|
318
|
+
// node IS its page, so the subtitle would be redundant there).
|
|
319
|
+
const showPage = node.kind !== 'pageview' && node.paths && node.paths.length > 0;
|
|
320
|
+
const topPath = showPage ? node.paths[0].path : null;
|
|
321
|
+
const morePages = showPage ? node.paths.length - 1 : 0;
|
|
322
|
+
return (_jsxs("g", { "data-node": "true", onClick: (e) => { e.stopPropagation(); setSelectedNode(prev => prev === node.id ? null : node.id); }, style: { cursor: 'pointer' }, children: [_jsxs("title", { children: [`${node.label} · ${node.count}×`, node.paths && node.paths.length > 0
|
|
323
|
+
? `\nPages:\n` + node.paths.slice(0, 8).map(p => ` ${p.path} (${p.count}×)`).join('\n')
|
|
324
|
+
: ''] }), _jsx("rect", { x: x, y: y, width: NODE_W, height: NODE_H, rx: 6, fill: fill, fillOpacity: nodeAlpha, stroke: fill, strokeOpacity: nodeStrokeOpacity, strokeWidth: nodeStrokeW }), topPath ? (_jsxs(_Fragment, { children: [_jsx("text", { x: x + NODE_W / 2, y: y + 17, textAnchor: "middle", fontSize: "11", fontWeight: "600", fill: "#1e293b", children: label }), _jsxs("text", { x: x + NODE_W / 2, y: y + 31, textAnchor: "middle", fontSize: "8.5", fill: kindColor(node.kind), children: [shortPath(topPath), morePages > 0 ? ` +${morePages}` : ''] }), _jsxs("text", { x: x + NODE_W / 2, y: y + 46, textAnchor: "middle", fontSize: "9", fill: "#475569", children: [node.count, "\u00D7"] })] })) : (_jsxs(_Fragment, { children: [_jsx("text", { x: x + NODE_W / 2, y: y + NODE_H / 2 - 3, textAnchor: "middle", fontSize: "11", fontWeight: "600", fill: "#1e293b", children: label }), _jsxs("text", { x: x + NODE_W / 2, y: y + NODE_H / 2 + 12, textAnchor: "middle", fontSize: "9", fill: "#475569", children: [node.count, "\u00D7"] })] }))] }, node.id));
|
|
325
|
+
})] }), selectedNodeObj && (_jsxs("div", { className: "absolute top-3 left-3 max-w-xs rounded-lg border bg-white/95 shadow-sm backdrop-blur px-3 py-2", style: { pointerEvents: 'auto' }, onMouseDown: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm flex-shrink-0", style: { backgroundColor: kindColor(selectedNodeObj.kind) } }), _jsx("span", { className: "text-xs font-semibold text-gray-700 truncate", children: selectedNodeObj.label }), _jsxs("span", { className: "text-[10px] text-gray-400 ml-auto flex-shrink-0", children: [selectedNodeObj.count, "\u00D7"] })] }), selectedNodeObj.paths && selectedNodeObj.paths.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-[10px] uppercase tracking-wide text-gray-400 mb-1", children: "Pages" }), _jsx("ul", { className: "flex flex-col gap-0.5 max-h-48 overflow-auto", children: selectedNodeObj.paths.map((p) => (_jsxs("li", { className: "flex items-center gap-2 text-[11px]", children: [_jsx("span", { className: "font-mono text-gray-600 truncate", children: p.path }), _jsxs("span", { className: "text-gray-400 tabular-nums ml-auto flex-shrink-0", children: [p.count, "\u00D7"] })] }, p.path))) })] })) : (_jsx("p", { className: "text-[11px] text-gray-400", children: "No page attribution recorded." }))] }))] })] }));
|
|
242
326
|
}
|
|
243
327
|
function SessionDetail({ session, index, profile }) {
|
|
244
328
|
const [open, setOpen] = useState(index === 0);
|
|
@@ -250,7 +334,7 @@ function SessionDetail({ session, index, profile }) {
|
|
|
250
334
|
// ---------------------------------------------------------------------------
|
|
251
335
|
// JourneyPanel — main export
|
|
252
336
|
// ---------------------------------------------------------------------------
|
|
253
|
-
export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserProfiles }) {
|
|
337
|
+
export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserProfiles, multiSidePorts = true, backEdgeLoop = true }) {
|
|
254
338
|
const [result, setResult] = useState(null);
|
|
255
339
|
const [loading, setLoading] = useState(false);
|
|
256
340
|
const [error, setError] = useState(null);
|
|
@@ -262,6 +346,10 @@ export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserPro
|
|
|
262
346
|
// Behavior tab's single-user selection).
|
|
263
347
|
const [selectedUserId, setSelectedUserId] = useState('');
|
|
264
348
|
const effectiveUserId = selectedUserId || userId;
|
|
349
|
+
// Split-by-page: ask the server to key nodes by event+page (refetch on change).
|
|
350
|
+
const [splitByPage, setSplitByPage] = useState(false);
|
|
351
|
+
// Page-flow: server returns only pageviews, linked page→page (skips clicks).
|
|
352
|
+
const [pageFlow, setPageFlow] = useState(false);
|
|
265
353
|
function userLabel(id) {
|
|
266
354
|
const p = userProfiles.get(id);
|
|
267
355
|
return p?.name ?? p?.email ?? `${id.slice(0, 12)}…`;
|
|
@@ -277,6 +365,10 @@ export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserPro
|
|
|
277
365
|
params.set('user_id', effectiveUserId);
|
|
278
366
|
if (sessionId)
|
|
279
367
|
params.set('session_id', sessionId);
|
|
368
|
+
if (splitByPage)
|
|
369
|
+
params.set('split_by_page', '1');
|
|
370
|
+
if (pageFlow)
|
|
371
|
+
params.set('page_flow', '1');
|
|
280
372
|
fetch(`${apiBase}/analytics/journey?${params.toString()}`, { credentials: 'include' })
|
|
281
373
|
.then((r) => r.json().catch(() => null))
|
|
282
374
|
.then((body) => {
|
|
@@ -313,11 +405,11 @@ export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserPro
|
|
|
313
405
|
setLoading(false);
|
|
314
406
|
});
|
|
315
407
|
return () => { cancelled = true; };
|
|
316
|
-
}, [appId, apiBase, effectiveUserId, sessionId]);
|
|
408
|
+
}, [appId, apiBase, effectiveUserId, sessionId, splitByPage, pageFlow]);
|
|
317
409
|
const sessionCount = result?.sessions.length ?? 0;
|
|
318
410
|
const totalSteps = result?.sessions.reduce((s, sess) => s + sess.steps.length, 0) ?? 0;
|
|
319
|
-
// Single source of truth for the
|
|
320
|
-
// passed into the flow graph so
|
|
321
|
-
const
|
|
322
|
-
return (_jsxs("div", { className: "flex flex-col gap-4 p-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-4 text-xs text-gray-500", children: [_jsxs("span", { children: [_jsx("span", { className: "font-semibold text-gray-700", children: sessionCount }), " session", sessionCount !== 1 ? 's' : ''] }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold text-gray-700", children: totalSteps }), " total steps"] }),
|
|
411
|
+
// Single source of truth for the data-fetch controls — rendered in the toolbar,
|
|
412
|
+
// and passed into the flow graph so they stay reachable in graph fullscreen.
|
|
413
|
+
const toolbarControls = (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex items-center gap-1 cursor-pointer select-none", title: "Show only page-to-page navigation (each page links to the next page visited)", children: [_jsx("input", { type: "checkbox", checked: pageFlow, onChange: (e) => setPageFlow(e.target.checked), className: "w-3 h-3 cursor-pointer", style: { accentColor: '#10b981' } }), "Pages only"] }), _jsxs("label", { className: "flex items-center gap-1 cursor-pointer select-none", title: "Split each event into one box per page", children: [_jsx("input", { type: "checkbox", checked: splitByPage, onChange: (e) => setSplitByPage(e.target.checked), className: "w-3 h-3 cursor-pointer" }), "Group by page"] }), knownUserIds.length > 0 && (_jsxs(_Fragment, { children: [_jsx("label", { className: "text-gray-500 ml-1", children: "User" }), _jsxs("select", { value: selectedUserId, onChange: (e) => setSelectedUserId(e.target.value), className: "rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-indigo-400", children: [_jsx("option", { value: "", children: "All users" }), knownUserIds.map((id) => (_jsx("option", { value: id, children: userLabel(id) }, id)))] })] }))] }));
|
|
414
|
+
return (_jsxs("div", { className: "flex flex-col gap-4 p-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-4 text-xs text-gray-500", children: [_jsxs("span", { children: [_jsx("span", { className: "font-semibold text-gray-700", children: sessionCount }), " session", sessionCount !== 1 ? 's' : ''] }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold text-gray-700", children: totalSteps }), " total steps"] }), _jsx("div", { className: "ml-auto flex items-center gap-3", children: toolbarControls })] }), error && (_jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-3 text-xs text-red-700", children: error })), loading && !result && (_jsx("div", { className: "p-4 text-xs text-gray-400", children: "Loading journey data\u2026" })), result && (_jsxs(_Fragment, { children: [result.graph.nodes.length > 0 && (_jsx(JourneyFlowGraph, { graph: result.graph, userFilter: toolbarControls, loading: loading, multiSidePorts: multiSidePorts, backEdgeLoop: backEdgeLoop })), result.sessions.length === 0 ? (_jsx("div", { className: "rounded-lg border p-6 text-center text-sm text-gray-400", children: "No journey sessions found." })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("h3", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide", children: "Sessions" }), result.sessions.map((session, i) => (_jsx(SessionDetail, { session: session, index: i, profile: session.user_id ? userProfiles.get(session.user_id) : undefined }, session.session_id)))] }))] })), !loading && !result && !error && (_jsx("div", { className: "rounded-lg border p-6 text-center text-sm text-gray-400", children: "No data yet." }))] }));
|
|
323
415
|
}
|