hazo_umetrics 1.5.1 → 1.6.2
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/server/activity.d.ts +1 -0
- package/dist/server/activity.d.ts.map +1 -1
- package/dist/server/activity.js +5 -0
- package/dist/ui/journey_panel.d.ts.map +1 -1
- package/dist/ui/journey_panel.js +106 -9
- package/dist/ui/metrics_panel.d.ts +3 -1
- package/dist/ui/metrics_panel.d.ts.map +1 -1
- package/dist/ui/metrics_panel.js +27 -3
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/server/activity.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAsB9D,MAAM,MAAM,QAAQ,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,UAAU,EAAE,QAAQ,EAAE,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AA8DvD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IACN,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/server/activity.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAsB9D,MAAM,MAAM,QAAQ,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,UAAU,EAAE,QAAQ,EAAE,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AA8DvD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IACN,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,CAAC,eAAe,CAAC,CAwC1B;AAMD;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IACN,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,CAAC,UAAU,EAAE,CAAC,CAqBvB"}
|
package/dist/server/activity.js
CHANGED
|
@@ -63,6 +63,7 @@ export async function getActivitySummary(adapter, params) {
|
|
|
63
63
|
const rows = await fetchWindow(adapter, { app_id: params.app_id, scope_id: params.scope_id, since, cap });
|
|
64
64
|
const uiRows = rows.filter((r) => r.metric_key.startsWith('ui.'));
|
|
65
65
|
const sessions = new Set();
|
|
66
|
+
const userIds = new Set();
|
|
66
67
|
const pageTally = new Map();
|
|
67
68
|
const btnTally = new Map();
|
|
68
69
|
let pageViews = 0;
|
|
@@ -72,6 +73,9 @@ export async function getActivitySummary(adapter, params) {
|
|
|
72
73
|
const sid = dims.session_id;
|
|
73
74
|
if (sid)
|
|
74
75
|
sessions.add(sid);
|
|
76
|
+
const uid = dims.user_id;
|
|
77
|
+
if (uid)
|
|
78
|
+
userIds.add(uid);
|
|
75
79
|
if (row.metric_key.startsWith('ui.pageview.')) {
|
|
76
80
|
pageViews++;
|
|
77
81
|
const path = dims.path ?? row.metric_key.replace('ui.pageview.', '/');
|
|
@@ -85,6 +89,7 @@ export async function getActivitySummary(adapter, params) {
|
|
|
85
89
|
}
|
|
86
90
|
return {
|
|
87
91
|
activeUsers: sessions.size,
|
|
92
|
+
activeUserIds: Array.from(userIds),
|
|
88
93
|
pageViews,
|
|
89
94
|
clicks,
|
|
90
95
|
topPages: topN(pageTally, 5),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journey_panel.d.ts","sourceRoot":"","sources":["../../src/ui/journey_panel.tsx"],"names":[],"mappings":"
|
|
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;IAChD,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,CAAC;IAChD,KAAK,EAAE,MAAM,CAAC;CACf;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;CAC3I;AAiaD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,mBAAmB,EAAE,EAAE,iBAAiB,+BAoJzG"}
|
package/dist/ui/journey_panel.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import { MousePointer2, Maximize2, Minimize2 } from 'lucide-react';
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Helpers
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
@@ -21,6 +22,7 @@ function kindColor(kind) {
|
|
|
21
22
|
default: return '#94a3b8';
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
function clampZoom(z) { return Math.max(0.25, Math.min(3, z)); }
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
25
27
|
// JourneyFlowGraph — SVG left-to-right DAG visualization
|
|
26
28
|
// ---------------------------------------------------------------------------
|
|
@@ -101,7 +103,51 @@ function computeFlowLayout(nodes, edges) {
|
|
|
101
103
|
}
|
|
102
104
|
return pos;
|
|
103
105
|
}
|
|
104
|
-
function JourneyFlowGraph({ graph }) {
|
|
106
|
+
function JourneyFlowGraph({ graph, userFilter }) {
|
|
107
|
+
const [zoom, setZoom] = useState(1.0);
|
|
108
|
+
const [pan, setPan] = useState({ x: 0, y: 0 });
|
|
109
|
+
const [dragging, setDragging] = useState(false);
|
|
110
|
+
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
|
111
|
+
const [selectedNode, setSelectedNode] = useState(null);
|
|
112
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
113
|
+
const wrapperRef = useRef(null);
|
|
114
|
+
const containerRef = useRef(null);
|
|
115
|
+
// Holds latest fitToScreen so the fullscreenchange effect can call it without
|
|
116
|
+
// re-subscribing every render (fitToScreen closes over per-render svg dims).
|
|
117
|
+
const fitToScreenRef = useRef(null);
|
|
118
|
+
// Track native fullscreen state (covers Esc-to-exit and the toggle button alike).
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const onFsChange = () => {
|
|
121
|
+
const active = document.fullscreenElement === wrapperRef.current;
|
|
122
|
+
setIsFullscreen(active);
|
|
123
|
+
// Refit once the card has resized to fill the screen.
|
|
124
|
+
if (active)
|
|
125
|
+
requestAnimationFrame(() => fitToScreenRef.current?.());
|
|
126
|
+
};
|
|
127
|
+
document.addEventListener('fullscreenchange', onFsChange);
|
|
128
|
+
return () => document.removeEventListener('fullscreenchange', onFsChange);
|
|
129
|
+
}, []);
|
|
130
|
+
function toggleFullscreen() {
|
|
131
|
+
if (document.fullscreenElement) {
|
|
132
|
+
document.exitFullscreen().catch(() => { });
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
wrapperRef.current?.requestFullscreen().catch(() => { });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Non-passive wheel listener so preventDefault() actually suppresses page scroll.
|
|
139
|
+
// React 17+ registers onWheel as passive, which silently ignores preventDefault.
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const el = containerRef.current;
|
|
142
|
+
if (!el)
|
|
143
|
+
return;
|
|
144
|
+
const onWheel = (e) => {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
setZoom(z => clampZoom(z * (e.deltaY < 0 ? 1.1 : 0.9)));
|
|
147
|
+
};
|
|
148
|
+
el.addEventListener('wheel', onWheel, { passive: false });
|
|
149
|
+
return () => el.removeEventListener('wheel', onWheel);
|
|
150
|
+
}, []);
|
|
105
151
|
if (graph.nodes.length === 0)
|
|
106
152
|
return null;
|
|
107
153
|
const pos = computeFlowLayout(graph.nodes, graph.edges);
|
|
@@ -120,8 +166,43 @@ function JourneyFlowGraph({ graph }) {
|
|
|
120
166
|
const offsetY = minY < 0 ? -minY + PAD : PAD;
|
|
121
167
|
const svgH = maxY - minY + PAD * 2;
|
|
122
168
|
const svgW = maxX;
|
|
169
|
+
function handleMouseDown(e) {
|
|
170
|
+
if (e.target.closest('[data-node]'))
|
|
171
|
+
return;
|
|
172
|
+
setDragging(true);
|
|
173
|
+
setDragStart({ x: e.clientX - pan.x, y: e.clientY - pan.y });
|
|
174
|
+
}
|
|
175
|
+
function handleMouseMove(e) {
|
|
176
|
+
if (!dragging)
|
|
177
|
+
return;
|
|
178
|
+
setPan({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y });
|
|
179
|
+
}
|
|
180
|
+
function handleMouseUp() { setDragging(false); }
|
|
181
|
+
function fitToScreen() {
|
|
182
|
+
if (!containerRef.current)
|
|
183
|
+
return;
|
|
184
|
+
const cw = containerRef.current.clientWidth;
|
|
185
|
+
const ch = containerRef.current.clientHeight;
|
|
186
|
+
const scale = clampZoom(Math.min(cw / svgW, ch / svgH) * 0.9);
|
|
187
|
+
setZoom(scale);
|
|
188
|
+
setPan({ x: (cw - svgW * scale) / 2, y: (ch - svgH * scale) / 2 });
|
|
189
|
+
}
|
|
190
|
+
fitToScreenRef.current = fitToScreen;
|
|
191
|
+
const highlightedEdges = new Set();
|
|
192
|
+
const highlightedNodes = new Set();
|
|
193
|
+
if (selectedNode) {
|
|
194
|
+
highlightedNodes.add(selectedNode);
|
|
195
|
+
graph.edges.forEach((edge, i) => {
|
|
196
|
+
if (edge.from === selectedNode || edge.to === selectedNode) {
|
|
197
|
+
highlightedEdges.add(i);
|
|
198
|
+
highlightedNodes.add(edge.from);
|
|
199
|
+
highlightedNodes.add(edge.to);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const hasSelection = selectedNode !== null;
|
|
123
204
|
const maxCount = Math.max(...graph.nodes.map(n => n.count), 1);
|
|
124
|
-
return (_jsxs("div", { className: "rounded-lg border bg-white
|
|
205
|
+
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: ["(", graph.nodes.length, " nodes \u00B7 ", graph.edges.length, " transitions)"] }), 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: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm", style: { backgroundColor: '#6366f1' } }), " action"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm", style: { backgroundColor: '#10b981' } }), " pageview"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm", style: { backgroundColor: '#ef4444' } }), " error"] }), _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" }) })] })] })] }), _jsx("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" }) }) }), graph.edges.map((edge, i) => {
|
|
125
206
|
const from = pos.get(edge.from);
|
|
126
207
|
const to = pos.get(edge.to);
|
|
127
208
|
if (!from || !to)
|
|
@@ -131,9 +212,19 @@ function JourneyFlowGraph({ graph }) {
|
|
|
131
212
|
const x2 = to.x;
|
|
132
213
|
const y2 = to.y + offsetY + NODE_H / 2;
|
|
133
214
|
const cx = (x1 + x2) / 2;
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
215
|
+
const isHighlighted = hasSelection ? highlightedEdges.has(i) : true;
|
|
216
|
+
const edgeOpacity = isHighlighted ? Math.max(0.4, edge.count / maxCount) : 0.05;
|
|
217
|
+
const edgeStroke = isHighlighted && hasSelection ? '#6366f1' : '#94a3b8';
|
|
218
|
+
const edgeStrokeW = isHighlighted ? Math.max(1.5, Math.min(4, edge.count / maxCount * 4)) : 1;
|
|
219
|
+
return (_jsxs("g", { children: [_jsx("path", { d: `M${x1},${y1} C${cx},${y1} ${cx},${y2} ${x2},${y2}`, fill: "none", stroke: edgeStroke, strokeWidth: edgeStrokeW, strokeOpacity: edgeOpacity, markerEnd: "url(#arrow)" }), (() => {
|
|
220
|
+
// On selection, label every connected edge (even 1×) boldly so
|
|
221
|
+
// occurrence counts are readable; otherwise keep the faint >1 hint.
|
|
222
|
+
const selectedEdge = hasSelection && isHighlighted;
|
|
223
|
+
const showLabel = hasSelection ? isHighlighted : edge.count > 1;
|
|
224
|
+
if (!showLabel)
|
|
225
|
+
return null;
|
|
226
|
+
return (_jsxs("text", { x: cx, y: (y1 + y2) / 2 - 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"] }));
|
|
227
|
+
})()] }, i));
|
|
137
228
|
}), graph.nodes.map((node) => {
|
|
138
229
|
const p = pos.get(node.id);
|
|
139
230
|
if (!p)
|
|
@@ -141,9 +232,12 @@ function JourneyFlowGraph({ graph }) {
|
|
|
141
232
|
const x = p.x;
|
|
142
233
|
const y = p.y + offsetY;
|
|
143
234
|
const fill = kindColor(node.kind);
|
|
144
|
-
const
|
|
235
|
+
const isNodeHighlighted = hasSelection ? highlightedNodes.has(node.id) : true;
|
|
236
|
+
const nodeAlpha = isNodeHighlighted ? Math.max(0.55, node.count / maxCount) : 0.1;
|
|
237
|
+
const nodeStrokeW = node.id === selectedNode ? 2.5 : 1.5;
|
|
238
|
+
const nodeStrokeOpacity = isNodeHighlighted ? Math.min(1, nodeAlpha + 0.3) : 0.1;
|
|
145
239
|
const label = node.label.length > 18 ? node.label.slice(0, 17) + '…' : node.label;
|
|
146
|
-
return (_jsxs("g", { children: [_jsx("rect", { x: x, y: y, width: NODE_W, height: NODE_H, rx: 6, fill: fill, fillOpacity:
|
|
240
|
+
return (_jsxs("g", { "data-node": "true", onClick: (e) => { e.stopPropagation(); setSelectedNode(prev => prev === node.id ? null : node.id); }, style: { cursor: 'pointer' }, children: [_jsx("rect", { x: x, y: y, width: NODE_W, height: NODE_H, rx: 6, fill: fill, fillOpacity: nodeAlpha, stroke: fill, strokeOpacity: nodeStrokeOpacity, strokeWidth: nodeStrokeW }), _jsx("text", { x: x + NODE_W / 2, y: y + NODE_H / 2 - 5, textAnchor: "middle", fontSize: "11", fontWeight: "600", fill: "#1e293b", children: label }), _jsxs("text", { x: x + NODE_W / 2, y: y + NODE_H / 2 + 9, textAnchor: "middle", fontSize: "9", fill: "#475569", children: [node.count, "\u00D7"] })] }, node.id));
|
|
147
241
|
})] }) })] }));
|
|
148
242
|
}
|
|
149
243
|
function SessionDetail({ session, index, profile }) {
|
|
@@ -222,5 +316,8 @@ export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserPro
|
|
|
222
316
|
}, [appId, apiBase, effectiveUserId, sessionId]);
|
|
223
317
|
const sessionCount = result?.sessions.length ?? 0;
|
|
224
318
|
const totalSteps = result?.sessions.reduce((s, sess) => s + sess.steps.length, 0) ?? 0;
|
|
225
|
-
|
|
319
|
+
// Single source of truth for the user filter — rendered in the toolbar, and
|
|
320
|
+
// passed into the flow graph so it stays reachable in graph fullscreen.
|
|
321
|
+
const userFilterControl = knownUserIds.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("label", { className: "text-gray-500", 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)))] })] })) : null;
|
|
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"] }), userFilterControl && (_jsx("div", { className: "ml-auto flex items-center gap-1.5", children: userFilterControl }))] }), error && (_jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-3 text-xs text-red-700", children: error })), loading && (_jsx("div", { className: "p-4 text-xs text-gray-400", children: "Loading journey data\u2026" })), !loading && result && (_jsxs(_Fragment, { children: [result.graph.nodes.length > 0 && (_jsx(JourneyFlowGraph, { graph: result.graph, userFilter: userFilterControl })), 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." }))] }));
|
|
226
323
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UserProfileInfo } from './analytics_panel.js';
|
|
1
2
|
export interface StatCardsProps {
|
|
2
3
|
stats: Array<{
|
|
3
4
|
key: string;
|
|
@@ -28,6 +29,7 @@ export interface ExperimentListProps {
|
|
|
28
29
|
export declare function ExperimentList({ experiments }: ExperimentListProps): import("react").JSX.Element;
|
|
29
30
|
export interface MetricsPanelProps {
|
|
30
31
|
appId: string;
|
|
32
|
+
resolveUserProfiles?: (ids: string[]) => Promise<UserProfileInfo[]>;
|
|
31
33
|
}
|
|
32
|
-
export declare function MetricsPanel({ appId }: MetricsPanelProps): import("react").JSX.Element;
|
|
34
|
+
export declare function MetricsPanel({ appId, resolveUserProfiles }: MetricsPanelProps): import("react").JSX.Element;
|
|
33
35
|
//# sourceMappingURL=metrics_panel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics_panel.d.ts","sourceRoot":"","sources":["../../src/ui/metrics_panel.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"metrics_panel.d.ts","sourceRoot":"","sources":["../../src/ui/metrics_panel.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AA2D5D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC5F;AAWD,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,cAAc,+BAkBlD;AAMD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAQD,wBAAgB,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,cAAc,+BAmBxD;AA4CD,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnG;AAcD,wBAAgB,cAAc,CAAC,EAAE,WAAW,EAAE,EAAE,mBAAmB,+BAwBlE;AAMD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACrE;AAwBD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,iBAAiB,+BAyM7E"}
|
package/dist/ui/metrics_panel.js
CHANGED
|
@@ -64,10 +64,13 @@ const INITIAL_STATE = {
|
|
|
64
64
|
topButtons: [],
|
|
65
65
|
experiments: [],
|
|
66
66
|
};
|
|
67
|
-
export function MetricsPanel({ appId }) {
|
|
67
|
+
export function MetricsPanel({ appId, resolveUserProfiles }) {
|
|
68
68
|
const [state, setState] = useState(INITIAL_STATE);
|
|
69
69
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
70
70
|
const refresh = useCallback(() => setRefreshKey((k) => k + 1), []);
|
|
71
|
+
const [activeUserIds, setActiveUserIds] = useState([]);
|
|
72
|
+
const [userProfiles, setUserProfiles] = useState([]);
|
|
73
|
+
const [showUsers, setShowUsers] = useState(false);
|
|
71
74
|
useEffect(() => {
|
|
72
75
|
if (!appId)
|
|
73
76
|
return;
|
|
@@ -106,6 +109,19 @@ export function MetricsPanel({ appId }) {
|
|
|
106
109
|
];
|
|
107
110
|
topPages = summary.topPages ?? [];
|
|
108
111
|
topButtons = summary.topButtons ?? [];
|
|
112
|
+
const ids = summary.activeUserIds ?? [];
|
|
113
|
+
if (!cancelled) {
|
|
114
|
+
setActiveUserIds(ids);
|
|
115
|
+
if (resolveUserProfiles && ids.length > 0) {
|
|
116
|
+
resolveUserProfiles(ids).then(profiles => {
|
|
117
|
+
if (!cancelled)
|
|
118
|
+
setUserProfiles(profiles);
|
|
119
|
+
}).catch(() => { });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
setUserProfiles([]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
109
125
|
}
|
|
110
126
|
trend = actBody?.data?.trend ?? [];
|
|
111
127
|
}
|
|
@@ -120,7 +136,7 @@ export function MetricsPanel({ appId }) {
|
|
|
120
136
|
}
|
|
121
137
|
});
|
|
122
138
|
return () => { cancelled = true; };
|
|
123
|
-
}, [appId, refreshKey]);
|
|
139
|
+
}, [appId, refreshKey, resolveUserProfiles]);
|
|
124
140
|
// -------------------------------------------------------------------------
|
|
125
141
|
// Render states
|
|
126
142
|
// -------------------------------------------------------------------------
|
|
@@ -136,5 +152,13 @@ export function MetricsPanel({ appId }) {
|
|
|
136
152
|
// -------------------------------------------------------------------------
|
|
137
153
|
// Main render
|
|
138
154
|
// -------------------------------------------------------------------------
|
|
139
|
-
return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", {}), _jsxs("button", { onClick: refresh, className: "flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-muted transition-colors", title: "Refresh metrics", children: [_jsx(RotateCw, { className: "h-3.5 w-3.5" }), "Refresh"] })] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Activity (last 14 days)" }), _jsx(StatCards, { stats: state.stats })
|
|
155
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", {}), _jsxs("button", { onClick: refresh, className: "flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-muted transition-colors", title: "Refresh metrics", children: [_jsx(RotateCw, { className: "h-3.5 w-3.5" }), "Refresh"] })] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Activity (last 14 days)" }), _jsx(StatCards, { stats: state.stats }), activeUserIds.length > 0 && (_jsxs("div", { className: "mt-2", children: [_jsx("button", { onClick: () => setShowUsers((v) => !v), className: "text-xs text-indigo-500 hover:underline", children: showUsers ? 'Hide users' : 'View users' }), showUsers && (_jsxs("div", { className: "mt-3 space-y-2", children: [userProfiles.map((p) => (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [p.profile_picture_url
|
|
156
|
+
? _jsx("img", { src: p.profile_picture_url, className: "w-7 h-7 rounded-full", alt: "" })
|
|
157
|
+
: _jsx("div", { className: "w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center text-xs text-gray-500", children: (p.name || p.email || '?')[0].toUpperCase() }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-gray-800", children: p.name ?? p.email ?? p.user_id }), p.name && p.email && _jsx("div", { className: "text-xs text-gray-400", children: p.email })] })] }, p.user_id))), (() => {
|
|
158
|
+
const totalSessions = state.stats.find((s) => s.key === 'active_users')?.value ?? 0;
|
|
159
|
+
const anonSessions = Math.max(0, totalSessions - userProfiles.length);
|
|
160
|
+
return anonSessions > 0
|
|
161
|
+
? _jsxs("div", { className: "text-xs text-gray-400 pl-9", children: [anonSessions, " anonymous session", anonSessions > 1 ? 's' : ''] })
|
|
162
|
+
: null;
|
|
163
|
+
})()] }))] }))] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Active Users Trend" }), _jsx(StatTrend, { data: state.trend, label: "Active Users" })] }), _jsxs("div", { className: "grid grid-cols-1 gap-6 sm:grid-cols-2", children: [_jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Top Pages" }), _jsx(TopList, { items: state.topPages, empty: "No page views yet" })] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Top Buttons" }), _jsx(TopList, { items: state.topButtons, empty: "No clicks yet" })] })] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Experiments & Flags" }), _jsx(ExperimentList, { experiments: state.experiments })] })] }));
|
|
140
164
|
}
|