@pyric/ui 0.1.0-alpha.11 → 0.1.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -4
- package/src/agents/ContextWindowUsage.tsx +514 -0
- package/src/agents/EmptyState.tsx +27 -0
- package/src/agents/Fold.tsx +64 -0
- package/src/agents/Modal.tsx +65 -0
- package/src/agents/PulsingDot.tsx +28 -0
- package/src/agents/inbrowser-agent-usage.d.ts +141 -0
- package/src/agents/index.ts +28 -0
- package/src/auth/authApi.ts +72 -0
- package/src/auth/claims.ts +63 -0
- package/src/auth/components/AuthProviderToggles.tsx +147 -0
- package/src/auth/components/AuthSignInHelper.tsx +197 -0
- package/src/auth/components/AuthUserForm.tsx +328 -0
- package/src/auth/components/AuthUserList.tsx +219 -0
- package/src/auth/components/ClaimsField.tsx +50 -0
- package/src/auth/components/confirmActions.tsx +114 -0
- package/src/auth/controller.ts +173 -0
- package/src/auth/hooks/index.ts +36 -0
- package/src/auth/hooks/useAuthFlowHelper.ts +55 -0
- package/src/auth/hooks/useAuthProviderConfig.ts +139 -0
- package/src/auth/hooks/useAuthUserEditor.ts +76 -0
- package/src/auth/hooks/useAuthUsers.ts +154 -0
- package/src/auth/index.ts +42 -0
- package/src/auth/providers.ts +28 -0
- package/src/auth/reducers/userEditor.ts +186 -0
- package/src/events/components/ActivityActionItems.tsx +136 -0
- package/src/events/components/ActivityGrid.tsx +197 -0
- package/src/events/components/ActivityGridRow.tsx +65 -0
- package/src/events/components/ProposedChangeDiff.tsx +175 -0
- package/src/events/components/format.ts +21 -0
- package/src/events/components/index.ts +17 -0
- package/src/events/digest.ts +630 -0
- package/src/events/hooks/index.ts +9 -0
- package/src/events/hooks/useActivityDigest.ts +42 -0
- package/src/events/hooks/useActivityStream.ts +86 -0
- package/src/events/index.ts +39 -0
- package/src/events/types.ts +152 -0
- package/src/firestore/components/CollectionList.tsx +78 -0
- package/src/firestore/components/DeleteWithConfirm.tsx +88 -0
- package/src/firestore/components/DocumentEditor.tsx +350 -0
- package/src/firestore/components/DocumentList.tsx +217 -0
- package/src/firestore/components/DocumentPreview.tsx +265 -0
- package/src/firestore/components/FieldRenderer.tsx +25 -0
- package/src/firestore/components/QueryBuilder.tsx +181 -0
- package/src/firestore/components/ReferencePicker.tsx +212 -0
- package/src/firestore/components/TreeEntry.tsx +58 -0
- package/src/firestore/components/context.ts +25 -0
- package/src/firestore/fieldEditors/array.tsx +30 -0
- package/src/firestore/fieldEditors/boolean.tsx +39 -0
- package/src/firestore/fieldEditors/bytes.tsx +53 -0
- package/src/firestore/fieldEditors/geopoint.tsx +71 -0
- package/src/firestore/fieldEditors/map.tsx +33 -0
- package/src/firestore/fieldEditors/null.tsx +27 -0
- package/src/firestore/fieldEditors/number.tsx +43 -0
- package/src/firestore/fieldEditors/reference.tsx +87 -0
- package/src/firestore/fieldEditors/registry.ts +45 -0
- package/src/firestore/fieldEditors/string.tsx +33 -0
- package/src/firestore/fieldEditors/timestamp.tsx +75 -0
- package/src/firestore/fieldEditors/types.ts +68 -0
- package/src/firestore/fieldEditors/vector.tsx +142 -0
- package/src/firestore/firestoreApi.ts +86 -0
- package/src/firestore/hooks/coerceError.ts +34 -0
- package/src/firestore/hooks/index.ts +46 -0
- package/src/firestore/hooks/useCollectionList.ts +102 -0
- package/src/firestore/hooks/useDocumentEditor.ts +161 -0
- package/src/firestore/hooks/useDocumentList.ts +242 -0
- package/src/firestore/hooks/useDocumentSubcollections.ts +86 -0
- package/src/firestore/hooks/useFirestoreCollection.ts +51 -0
- package/src/firestore/hooks/useFirestoreDoc.ts +55 -0
- package/src/firestore/hooks/useQueryBuilder.ts +188 -0
- package/src/firestore/hooks/useRecursiveDelete.ts +77 -0
- package/src/firestore/hooks/useReferencePicker.ts +228 -0
- package/src/firestore/import/parseImport.ts +137 -0
- package/src/firestore/index.ts +87 -0
- package/src/firestore/reducers/defaults.ts +38 -0
- package/src/firestore/reducers/documentEditor.ts +239 -0
- package/src/firestore/reducers/tree.ts +140 -0
- package/src/firestore/reducers/types.ts +92 -0
- package/src/firestore/reducers/validation.ts +129 -0
- package/src/firestore/types.ts +231 -0
- package/src/firestore/validation/ids.ts +45 -0
- package/src/firestore/valueEquality.ts +43 -0
- package/src/index.ts +10 -0
- package/src/primitives/Badge.tsx +40 -0
- package/src/primitives/ConfirmDialog.tsx +137 -0
- package/src/primitives/CopyButton.tsx +62 -0
- package/src/primitives/JsonView.tsx +151 -0
- package/src/primitives/SegmentedControl.tsx +72 -0
- package/src/primitives/Toast.tsx +161 -0
- package/src/primitives/VirtualList.tsx +104 -0
- package/src/primitives/hooks/useContainerSize.ts +53 -0
- package/src/primitives/hooks/useUpdateHighlights.ts +109 -0
- package/src/primitives/index.ts +39 -0
- package/src/primitives/useConfirm.tsx +113 -0
- package/src/rtdb/components/RtdbPathBar.tsx +135 -0
- package/src/rtdb/components/RtdbTree.tsx +409 -0
- package/src/rtdb/editor.ts +79 -0
- package/src/rtdb/hooks/useRtdbTree.ts +137 -0
- package/src/rtdb/index.ts +66 -0
- package/src/rtdb/pathInput.ts +47 -0
- package/src/rtdb/reducers/tree.ts +191 -0
- package/src/rtdb/rtdbApi.ts +23 -0
- package/src/rtdb/values.ts +188 -0
- package/src/rules/components/DenialInspector.tsx +227 -0
- package/src/rules/components/format.ts +171 -0
- package/src/rules/components/index.ts +12 -0
- package/src/rules/components/scope.ts +75 -0
- package/src/rules/hooks/index.ts +5 -0
- package/src/rules/hooks/useDenialTrace.ts +100 -0
- package/src/rules/index.ts +24 -0
- package/src/rules/types.ts +91 -0
- package/src/storage/collisionRename.ts +114 -0
- package/src/storage/components/DeleteSelectionWithConfirm.tsx +193 -0
- package/src/storage/components/ObjectBrowser.tsx +219 -0
- package/src/storage/components/ObjectInspector.tsx +169 -0
- package/src/storage/components/PathBreadcrumb.tsx +84 -0
- package/src/storage/components/UploadDropzone.tsx +182 -0
- package/src/storage/folderPlaceholder.ts +40 -0
- package/src/storage/hooks/index.ts +59 -0
- package/src/storage/hooks/useMetadataEditor.ts +329 -0
- package/src/storage/hooks/useObjectUpload.ts +262 -0
- package/src/storage/hooks/usePathState.ts +94 -0
- package/src/storage/hooks/useStorageDelete.ts +195 -0
- package/src/storage/hooks/useStorageList.ts +261 -0
- package/src/storage/hooks/useStorageObject.ts +162 -0
- package/src/storage/hooks/useStorageRulesGate.ts +270 -0
- package/src/storage/hooks/useStorageSelection.ts +90 -0
- package/src/storage/index.ts +59 -0
- package/src/storage/pendingPrefixes.ts +125 -0
- package/src/storage/previews.tsx +120 -0
- package/src/storage/storageApi.ts +54 -0
- package/src/traffic/components/RuleHeatmap.tsx +97 -0
- package/src/traffic/components/TrafficDetail.tsx +160 -0
- package/src/traffic/components/TrafficGroupRow.tsx +91 -0
- package/src/traffic/components/TrafficLineChart.tsx +139 -0
- package/src/traffic/components/TrafficLog.tsx +175 -0
- package/src/traffic/components/TrafficMetricCards.tsx +77 -0
- package/src/traffic/components/TrafficRow.tsx +69 -0
- package/src/traffic/components/TrafficStats.tsx +73 -0
- package/src/traffic/components/TrafficTimeline.tsx +289 -0
- package/src/traffic/components/format.ts +22 -0
- package/src/traffic/components/index.ts +22 -0
- package/src/traffic/hooks/index.ts +60 -0
- package/src/traffic/hooks/useRuleHeatmap.ts +92 -0
- package/src/traffic/hooks/useTrafficBuckets.ts +146 -0
- package/src/traffic/hooks/useTrafficFilter.ts +74 -0
- package/src/traffic/hooks/useTrafficGroups.ts +126 -0
- package/src/traffic/hooks/useTrafficMetrics.ts +250 -0
- package/src/traffic/hooks/useTrafficMonitor.ts +111 -0
- package/src/traffic/hooks/useTrafficStats.ts +77 -0
- package/src/traffic/index.ts +13 -0
- package/src/traffic/types.ts +85 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { MetricPoint, MetricSeries } from '../hooks/useTrafficMetrics.js';
|
|
3
|
+
|
|
4
|
+
export interface TrafficLineChartProps {
|
|
5
|
+
points: readonly MetricPoint[];
|
|
6
|
+
series: readonly MetricSeries[];
|
|
7
|
+
/** Series keys currently drawn. Omit to draw every series. Pair with
|
|
8
|
+
* {@link TrafficMetricCards}' `visible`/`onToggle` so the legend cards
|
|
9
|
+
* and the chart's lines toggle in lockstep. */
|
|
10
|
+
visible?: ReadonlySet<string>;
|
|
11
|
+
/** Omit all-zero series from the plot and tooltip while retaining their
|
|
12
|
+
* explicit total in the accompanying metric strip. */
|
|
13
|
+
omitZeroSeries?: boolean;
|
|
14
|
+
formatValue?: (n: number) => string;
|
|
15
|
+
formatTime?: (t: number) => string;
|
|
16
|
+
emptyState?: React.ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultFormatValue = (n: number) => String(n);
|
|
21
|
+
const defaultFormatTime = (t: number) => new Date(t).toLocaleTimeString();
|
|
22
|
+
|
|
23
|
+
/** Hand-rolled SVG line chart, no charting dependency — plain lines over
|
|
24
|
+
* a 0..100 viewBox so CSS drives the actual size (intrinsic layout, no
|
|
25
|
+
* fixed pixel chart). Each series draws only when `visible` includes
|
|
26
|
+
* its key (or `visible` is omitted). The y-scale is the max value
|
|
27
|
+
* across the currently VISIBLE series only, so toggling a tall series
|
|
28
|
+
* off rescales the rest up — matching the Console reference.
|
|
29
|
+
*
|
|
30
|
+
* Interaction: hovering (or focusing, via the invisible per-bucket hit
|
|
31
|
+
* targets) shows a tooltip with the bucket's time range and each visible
|
|
32
|
+
* series' value at that bucket — `[data-pyric-chart-tooltip]`, positioned
|
|
33
|
+
* via `--pyric-hover-x`.
|
|
34
|
+
*
|
|
35
|
+
* Styling hooks: `[data-pyric-ui="traffic-line-chart"]`,
|
|
36
|
+
* `[data-pyric-chart-svg]`, `[data-pyric-chart-line]` (with
|
|
37
|
+
* `data-pyric-series-key`, `data-pyric-series-index` — the same index a
|
|
38
|
+
* `TrafficMetricCards` card carries, so line + card colors line up),
|
|
39
|
+
* `[data-pyric-chart-hit]` (with `data-pyric-point-index`),
|
|
40
|
+
* `[data-pyric-chart-tooltip]`, `[data-pyric-tooltip-row]`.
|
|
41
|
+
*/
|
|
42
|
+
export function TrafficLineChart({
|
|
43
|
+
points,
|
|
44
|
+
series,
|
|
45
|
+
visible,
|
|
46
|
+
omitZeroSeries = false,
|
|
47
|
+
formatValue = defaultFormatValue,
|
|
48
|
+
formatTime = defaultFormatTime,
|
|
49
|
+
emptyState,
|
|
50
|
+
className,
|
|
51
|
+
}: TrafficLineChartProps) {
|
|
52
|
+
const [hovered, setHovered] = useState<number | null>(null);
|
|
53
|
+
|
|
54
|
+
if (points.length === 0) {
|
|
55
|
+
return (
|
|
56
|
+
<div className={className} data-pyric-ui="traffic-line-chart" data-pyric-empty="">
|
|
57
|
+
{emptyState}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const visibleSeries = series.filter(
|
|
63
|
+
(s) => (!visible || visible.has(s.key)) && (!omitZeroSeries || s.total > 0),
|
|
64
|
+
);
|
|
65
|
+
let maxValue = 0;
|
|
66
|
+
for (const s of visibleSeries) for (const v of s.values) if (v > maxValue) maxValue = v;
|
|
67
|
+
const scale = maxValue === 0 ? 1 : maxValue;
|
|
68
|
+
|
|
69
|
+
const n = points.length;
|
|
70
|
+
const xAt = (i: number) => (n === 1 ? 50 : (i / (n - 1)) * 100);
|
|
71
|
+
const yAt = (v: number) => 100 - (v / scale) * 100;
|
|
72
|
+
|
|
73
|
+
const hoveredPoint = hovered != null ? points[hovered] : null;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className={className} data-pyric-ui="traffic-line-chart">
|
|
77
|
+
<div data-pyric-chart-svg="">
|
|
78
|
+
<svg viewBox="0 0 100 100" preserveAspectRatio="none" role="img" aria-label="metrics over time">
|
|
79
|
+
{series.map((s) => {
|
|
80
|
+
const seriesIndex = series.indexOf(s);
|
|
81
|
+
const isVisible =
|
|
82
|
+
(!visible || visible.has(s.key)) && (!omitZeroSeries || s.total > 0);
|
|
83
|
+
if (!isVisible) return null;
|
|
84
|
+
const d = s.values.map((v, i) => `${i === 0 ? 'M' : 'L'}${xAt(i)},${yAt(v)}`).join(' ');
|
|
85
|
+
return (
|
|
86
|
+
<path
|
|
87
|
+
key={s.key}
|
|
88
|
+
data-pyric-chart-line=""
|
|
89
|
+
data-pyric-series-key={s.key}
|
|
90
|
+
data-pyric-series-index={seriesIndex}
|
|
91
|
+
d={d}
|
|
92
|
+
fill="none"
|
|
93
|
+
vectorEffect="non-scaling-stroke"
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
97
|
+
</svg>
|
|
98
|
+
|
|
99
|
+
<div data-pyric-chart-hits="">
|
|
100
|
+
{points.map((point, i) => (
|
|
101
|
+
<button
|
|
102
|
+
key={point.index}
|
|
103
|
+
type="button"
|
|
104
|
+
data-pyric-chart-hit=""
|
|
105
|
+
data-pyric-point-index={i}
|
|
106
|
+
onMouseEnter={() => setHovered(i)}
|
|
107
|
+
onFocus={() => setHovered(i)}
|
|
108
|
+
onMouseLeave={() => setHovered((cur) => (cur === i ? null : cur))}
|
|
109
|
+
onBlur={() => setHovered((cur) => (cur === i ? null : cur))}
|
|
110
|
+
aria-label={`${formatTime(point.start)} to ${formatTime(point.end)}`}
|
|
111
|
+
/>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{hoveredPoint && (
|
|
116
|
+
<div
|
|
117
|
+
data-pyric-chart-tooltip=""
|
|
118
|
+
style={{ '--pyric-hover-x': xAt(hoveredPoint.index) } as React.CSSProperties}
|
|
119
|
+
>
|
|
120
|
+
<div data-pyric-tooltip-time="">
|
|
121
|
+
{formatTime(hoveredPoint.start)} – {formatTime(hoveredPoint.end)}
|
|
122
|
+
</div>
|
|
123
|
+
{visibleSeries.map((s) => (
|
|
124
|
+
<div
|
|
125
|
+
key={s.key}
|
|
126
|
+
data-pyric-tooltip-row=""
|
|
127
|
+
data-pyric-series-key={s.key}
|
|
128
|
+
data-pyric-series-index={series.indexOf(s)}
|
|
129
|
+
>
|
|
130
|
+
<span data-pyric-tooltip-label="">{s.label}</span>
|
|
131
|
+
<span data-pyric-tooltip-value="">{formatValue(s.values[hoveredPoint.index])}</span>
|
|
132
|
+
</div>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { VirtualList } from '../../primitives/VirtualList.js';
|
|
3
|
+
import type { TrafficEvent } from '../types.js';
|
|
4
|
+
import type { TrafficLogItem } from '../hooks/useTrafficGroups.js';
|
|
5
|
+
import { TrafficRow } from './TrafficRow.js';
|
|
6
|
+
import { TrafficGroupRow } from './TrafficGroupRow.js';
|
|
7
|
+
|
|
8
|
+
export interface TrafficLogProps {
|
|
9
|
+
/** Events to render, in display order. The hook layer decides the
|
|
10
|
+
* order — `<TrafficLog>` renders the array as given. */
|
|
11
|
+
events: TrafficEvent[];
|
|
12
|
+
/**
|
|
13
|
+
* Grouped items from `useTrafficGroups`. When provided, the log
|
|
14
|
+
* renders singles + collapsible group rows and does NOT
|
|
15
|
+
* virtualize — grouping is itself the volume reducer (a 250-event
|
|
16
|
+
* listener storm becomes one group row). `events` is ignored.
|
|
17
|
+
*/
|
|
18
|
+
items?: TrafficLogItem[];
|
|
19
|
+
/** The selected event's id, marked with `data-pyric-selected`. */
|
|
20
|
+
selectedId?: string;
|
|
21
|
+
onSelect?: (event: TrafficEvent) => void;
|
|
22
|
+
/** Passed through to each `<TrafficRow>`. */
|
|
23
|
+
renderClassification?: (event: TrafficEvent) => ReactNode;
|
|
24
|
+
/** Passed through to each `<TrafficRow>`. */
|
|
25
|
+
formatTime?: (at: number) => string;
|
|
26
|
+
/**
|
|
27
|
+
* Full escape hatch — render a row yourself instead of the default
|
|
28
|
+
* `<TrafficRow>`. Receives the event and its selected state.
|
|
29
|
+
* Applies to the flat (`events`) path only.
|
|
30
|
+
*/
|
|
31
|
+
renderRow?: (event: TrafficEvent, selected: boolean) => ReactNode;
|
|
32
|
+
emptyState?: ReactNode;
|
|
33
|
+
className?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Above this row count, the list virtualizes via `<VirtualList>`.
|
|
36
|
+
* Default 100 — a `load-test`-shaped session can emit 100k+ events,
|
|
37
|
+
* so virtualization is load-bearing, not polish.
|
|
38
|
+
*/
|
|
39
|
+
virtualizeThreshold?: number;
|
|
40
|
+
/** Estimated row height when virtualizing. Default 28. */
|
|
41
|
+
rowHeight?: number | ((index: number) => number);
|
|
42
|
+
/** Pixel height the virtualized scroll container fills. Default
|
|
43
|
+
* `'60vh'`. */
|
|
44
|
+
virtualizedHeight?: number | string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Headless traffic log — a Chrome DevTools Network-panel-style event
|
|
49
|
+
* stream. Below `virtualizeThreshold` it renders a plain `<ul>`;
|
|
50
|
+
* above it, TanStack-Virtual via `<VirtualList>`.
|
|
51
|
+
*
|
|
52
|
+
* Styling hooks: `[data-pyric-ui="traffic-log"]`,
|
|
53
|
+
* `[data-pyric-traffic-entry]` (the list item wrapper), plus the
|
|
54
|
+
* `<TrafficRow>` hooks.
|
|
55
|
+
*/
|
|
56
|
+
export function TrafficLog({
|
|
57
|
+
events,
|
|
58
|
+
items,
|
|
59
|
+
selectedId,
|
|
60
|
+
onSelect,
|
|
61
|
+
renderClassification,
|
|
62
|
+
formatTime,
|
|
63
|
+
renderRow,
|
|
64
|
+
emptyState,
|
|
65
|
+
className,
|
|
66
|
+
virtualizeThreshold = 100,
|
|
67
|
+
rowHeight = 28,
|
|
68
|
+
virtualizedHeight = '60vh',
|
|
69
|
+
}: TrafficLogProps) {
|
|
70
|
+
// Grouped mode — singles + collapsible group rows, no virtualization.
|
|
71
|
+
if (items !== undefined) {
|
|
72
|
+
if (items.length === 0) {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
className={className}
|
|
76
|
+
data-pyric-ui="traffic-log"
|
|
77
|
+
data-pyric-empty=""
|
|
78
|
+
>
|
|
79
|
+
{emptyState}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return (
|
|
84
|
+
<div className={className} data-pyric-ui="traffic-log" data-pyric-grouped="">
|
|
85
|
+
<ul data-pyric-traffic-log-items="">
|
|
86
|
+
{items.map((item) =>
|
|
87
|
+
item.type === 'group' ? (
|
|
88
|
+
<li key={item.key} data-pyric-traffic-group-entry="">
|
|
89
|
+
<TrafficGroupRow
|
|
90
|
+
group={item}
|
|
91
|
+
onSelect={onSelect}
|
|
92
|
+
selectedId={selectedId}
|
|
93
|
+
renderClassification={renderClassification}
|
|
94
|
+
formatTime={formatTime}
|
|
95
|
+
/>
|
|
96
|
+
</li>
|
|
97
|
+
) : (
|
|
98
|
+
<li
|
|
99
|
+
key={item.event.id}
|
|
100
|
+
data-pyric-traffic-entry=""
|
|
101
|
+
data-pyric-traffic-id={item.event.id}
|
|
102
|
+
>
|
|
103
|
+
<TrafficRow
|
|
104
|
+
event={item.event}
|
|
105
|
+
selected={item.event.id === selectedId}
|
|
106
|
+
onSelect={onSelect}
|
|
107
|
+
renderClassification={renderClassification}
|
|
108
|
+
formatTime={formatTime}
|
|
109
|
+
/>
|
|
110
|
+
</li>
|
|
111
|
+
),
|
|
112
|
+
)}
|
|
113
|
+
</ul>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (events.length === 0) {
|
|
119
|
+
return (
|
|
120
|
+
<div className={className} data-pyric-ui="traffic-log" data-pyric-empty="">
|
|
121
|
+
{emptyState}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const row = (event: TrafficEvent): ReactNode => {
|
|
127
|
+
const selected = event.id === selectedId;
|
|
128
|
+
if (renderRow) return renderRow(event, selected);
|
|
129
|
+
return (
|
|
130
|
+
<TrafficRow
|
|
131
|
+
event={event}
|
|
132
|
+
selected={selected}
|
|
133
|
+
onSelect={onSelect}
|
|
134
|
+
renderClassification={renderClassification}
|
|
135
|
+
formatTime={formatTime}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const virtualized = events.length > virtualizeThreshold;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
className={className}
|
|
145
|
+
data-pyric-ui="traffic-log"
|
|
146
|
+
data-pyric-virtualized={virtualized ? '' : undefined}
|
|
147
|
+
>
|
|
148
|
+
{virtualized ? (
|
|
149
|
+
<VirtualList
|
|
150
|
+
items={events}
|
|
151
|
+
estimateSize={rowHeight}
|
|
152
|
+
height={virtualizedHeight}
|
|
153
|
+
getItemKey={(event) => event.id}
|
|
154
|
+
renderItem={(event) => (
|
|
155
|
+
<div data-pyric-traffic-entry="" data-pyric-traffic-id={event.id}>
|
|
156
|
+
{row(event)}
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
/>
|
|
160
|
+
) : (
|
|
161
|
+
<ul data-pyric-traffic-log-items="">
|
|
162
|
+
{events.map((event) => (
|
|
163
|
+
<li
|
|
164
|
+
key={event.id}
|
|
165
|
+
data-pyric-traffic-entry=""
|
|
166
|
+
data-pyric-traffic-id={event.id}
|
|
167
|
+
>
|
|
168
|
+
{row(event)}
|
|
169
|
+
</li>
|
|
170
|
+
))}
|
|
171
|
+
</ul>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { MetricSeries } from '../hooks/useTrafficMetrics.js';
|
|
2
|
+
|
|
3
|
+
export interface TrafficMetricCardsProps {
|
|
4
|
+
series: readonly MetricSeries[];
|
|
5
|
+
/** When supplied (with `onToggle`), each card gets a checkbox and
|
|
6
|
+
* doubles as the chart's legend — Console reference semantics
|
|
7
|
+
* ("toggle series visibility"). Omit for a read-only totals strip. */
|
|
8
|
+
visible?: ReadonlySet<string>;
|
|
9
|
+
onToggle?: (key: string) => void;
|
|
10
|
+
formatValue?: (n: number) => string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const defaultFormatValue = (n: number) => String(n);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The period-total strip: one card per series, each showing the series'
|
|
18
|
+
* total for the current window. Doubles as BOTH the chart's legend row
|
|
19
|
+
* (pass `visible` + `onToggle` to get the checkbox-toggle semantics) AND
|
|
20
|
+
* the standalone fallback presentation when a chart isn't warranted —
|
|
21
|
+
* this component never depends on {@link TrafficLineChart}.
|
|
22
|
+
*
|
|
23
|
+
* Styling hooks: `[data-pyric-ui="traffic-metric-cards"]`,
|
|
24
|
+
* `[data-pyric-metric-card]` (with `data-pyric-metric-key`,
|
|
25
|
+
* `data-pyric-series-index` — the color channel a chart's lines also
|
|
26
|
+
* key off of, so a card and its line always match), `[data-pyric-metric-hidden]`
|
|
27
|
+
* when toggled off.
|
|
28
|
+
*/
|
|
29
|
+
export function TrafficMetricCards({
|
|
30
|
+
series,
|
|
31
|
+
visible,
|
|
32
|
+
onToggle,
|
|
33
|
+
formatValue = defaultFormatValue,
|
|
34
|
+
className,
|
|
35
|
+
}: TrafficMetricCardsProps) {
|
|
36
|
+
const toggleable = visible !== undefined && onToggle !== undefined;
|
|
37
|
+
return (
|
|
38
|
+
<div className={className} data-pyric-ui="traffic-metric-cards" role={toggleable ? 'group' : undefined}>
|
|
39
|
+
{series.map((s, index) => {
|
|
40
|
+
const isVisible = visible ? visible.has(s.key) : true;
|
|
41
|
+
const card = (
|
|
42
|
+
<>
|
|
43
|
+
<span data-pyric-metric-swatch="" aria-hidden="true" />
|
|
44
|
+
<span data-pyric-metric-label="">{s.label}</span>
|
|
45
|
+
<span data-pyric-metric-value="">{formatValue(s.total)}</span>
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
return toggleable ? (
|
|
49
|
+
<label
|
|
50
|
+
key={s.key}
|
|
51
|
+
data-pyric-metric-card=""
|
|
52
|
+
data-pyric-metric-key={s.key}
|
|
53
|
+
data-pyric-series-index={index}
|
|
54
|
+
data-pyric-metric-hidden={isVisible ? undefined : ''}
|
|
55
|
+
>
|
|
56
|
+
<input
|
|
57
|
+
type="checkbox"
|
|
58
|
+
checked={isVisible}
|
|
59
|
+
onChange={() => onToggle!(s.key)}
|
|
60
|
+
data-pyric-metric-checkbox=""
|
|
61
|
+
/>
|
|
62
|
+
{card}
|
|
63
|
+
</label>
|
|
64
|
+
) : (
|
|
65
|
+
<span
|
|
66
|
+
key={s.key}
|
|
67
|
+
data-pyric-metric-card=""
|
|
68
|
+
data-pyric-metric-key={s.key}
|
|
69
|
+
data-pyric-series-index={index}
|
|
70
|
+
>
|
|
71
|
+
{card}
|
|
72
|
+
</span>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Badge } from '../../primitives/Badge.js';
|
|
3
|
+
import type { TrafficEvent } from '../types.js';
|
|
4
|
+
import { defaultFormatTime } from './format.js';
|
|
5
|
+
|
|
6
|
+
export interface TrafficRowProps {
|
|
7
|
+
event: TrafficEvent;
|
|
8
|
+
/** Marks the row as the active selection (`data-pyric-selected`). */
|
|
9
|
+
selected?: boolean;
|
|
10
|
+
onSelect?: (event: TrafficEvent) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Render-prop slot for a consumer-specific classification badge —
|
|
13
|
+
* e.g. the playground's `expected`/`ambiguous`/`unexpected`
|
|
14
|
+
* verdict, which is app-source analysis the library doesn't own.
|
|
15
|
+
* Returns `null` to render nothing.
|
|
16
|
+
*/
|
|
17
|
+
renderClassification?: (event: TrafficEvent) => ReactNode;
|
|
18
|
+
/** Override the timestamp rendering. Default is `HH:MM:SS`. */
|
|
19
|
+
formatTime?: (at: number) => string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One headless traffic row — timestamp, method badge, path, and an
|
|
25
|
+
* optional consumer classification slot. The raw result is exposed as
|
|
26
|
+
* `data-pyric-result` on the row (for tinting/filtering); rendering a
|
|
27
|
+
* verdict/outcome label is the consumer's job via
|
|
28
|
+
* `renderClassification` — the row itself draws no result chip, so a
|
|
29
|
+
* consumer verdict never collides with a built-in one. Latency is
|
|
30
|
+
* intentionally absent: this is a local simulator, so `evalMs` lives
|
|
31
|
+
* in `<TrafficDetail>` only.
|
|
32
|
+
*
|
|
33
|
+
* Styling hooks:
|
|
34
|
+
* - `[data-pyric-traffic-row]` — the row button, with
|
|
35
|
+
* `data-pyric-result`, `data-pyric-origin`, `data-pyric-method`
|
|
36
|
+
* - `[data-pyric-traffic-row][data-pyric-selected]` — active row
|
|
37
|
+
* - `[data-pyric-traffic-time]` / `[data-pyric-traffic-path]`
|
|
38
|
+
* - `[data-pyric-traffic-service]` — the service label (firestore / rtdb /
|
|
39
|
+
* storage / auth), rendered even when unknown (empty) so fixed-width
|
|
40
|
+
* styling keeps the columns aligned
|
|
41
|
+
* - method renders as `<Badge>` (`data-pyric-badge-kind`)
|
|
42
|
+
*/
|
|
43
|
+
export function TrafficRow({
|
|
44
|
+
event,
|
|
45
|
+
selected,
|
|
46
|
+
onSelect,
|
|
47
|
+
renderClassification,
|
|
48
|
+
formatTime = defaultFormatTime,
|
|
49
|
+
className,
|
|
50
|
+
}: TrafficRowProps) {
|
|
51
|
+
return (
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={() => onSelect?.(event)}
|
|
55
|
+
className={className}
|
|
56
|
+
data-pyric-traffic-row=""
|
|
57
|
+
data-pyric-result={event.result}
|
|
58
|
+
data-pyric-origin={event.origin}
|
|
59
|
+
data-pyric-method={event.method}
|
|
60
|
+
data-pyric-selected={selected ? '' : undefined}
|
|
61
|
+
>
|
|
62
|
+
<span data-pyric-traffic-time="">{formatTime(event.at)}</span>
|
|
63
|
+
<span data-pyric-traffic-service={event.service ?? ''}>{event.service ?? ''}</span>
|
|
64
|
+
<Badge kind={event.method}>{event.method}</Badge>
|
|
65
|
+
<span data-pyric-traffic-path="">{event.path || '/'}</span>
|
|
66
|
+
{renderClassification ? renderClassification(event) : null}
|
|
67
|
+
</button>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { TrafficStatsSummary } from '../hooks/useTrafficStats.js';
|
|
2
|
+
|
|
3
|
+
export interface TrafficStatsProps {
|
|
4
|
+
stats: TrafficStatsSummary;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function BucketList({
|
|
9
|
+
label,
|
|
10
|
+
buckets,
|
|
11
|
+
}: {
|
|
12
|
+
label: string;
|
|
13
|
+
buckets: TrafficStatsSummary['byMethod'];
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<div data-pyric-stat-group="" data-pyric-stat-group-label={label}>
|
|
17
|
+
<h4 data-pyric-stat-group-heading="">{label}</h4>
|
|
18
|
+
<ul data-pyric-stat-buckets="">
|
|
19
|
+
{buckets.map((b) => (
|
|
20
|
+
<li key={b.key} data-pyric-stat-bucket="" data-pyric-stat-key={b.key}>
|
|
21
|
+
<span data-pyric-stat-bucket-label="">{b.key}</span>
|
|
22
|
+
<span data-pyric-stat-bucket-count="">{b.count}</span>
|
|
23
|
+
</li>
|
|
24
|
+
))}
|
|
25
|
+
</ul>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Headless aggregation panel. Renders the totals, the deny rate, and
|
|
32
|
+
* count breakdowns by method / origin / path. The deny rate is
|
|
33
|
+
* exposed both as text and as a `--pyric-deny-rate` CSS custom
|
|
34
|
+
* property on the root for a proportional meter.
|
|
35
|
+
*
|
|
36
|
+
* Styling hooks: `[data-pyric-ui="traffic-stats"]`,
|
|
37
|
+
* `[data-pyric-stat]` (with `data-pyric-stat-key`),
|
|
38
|
+
* `[data-pyric-stat-group]` (with `data-pyric-stat-group-label`),
|
|
39
|
+
* `[data-pyric-stat-bucket]`.
|
|
40
|
+
*/
|
|
41
|
+
export function TrafficStats({ stats, className }: TrafficStatsProps) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className={className}
|
|
45
|
+
data-pyric-ui="traffic-stats"
|
|
46
|
+
style={{ '--pyric-deny-rate': stats.denyRate } as React.CSSProperties}
|
|
47
|
+
>
|
|
48
|
+
<div data-pyric-stat-totals="">
|
|
49
|
+
<span data-pyric-stat="" data-pyric-stat-key="total">
|
|
50
|
+
<span data-pyric-stat-label="">total</span>
|
|
51
|
+
<span data-pyric-stat-value="">{stats.total}</span>
|
|
52
|
+
</span>
|
|
53
|
+
<span data-pyric-stat="" data-pyric-stat-key="allows">
|
|
54
|
+
<span data-pyric-stat-label="">allowed</span>
|
|
55
|
+
<span data-pyric-stat-value="">{stats.allows}</span>
|
|
56
|
+
</span>
|
|
57
|
+
<span data-pyric-stat="" data-pyric-stat-key="denies">
|
|
58
|
+
<span data-pyric-stat-label="">denied</span>
|
|
59
|
+
<span data-pyric-stat-value="">{stats.denies}</span>
|
|
60
|
+
</span>
|
|
61
|
+
<span data-pyric-stat="" data-pyric-stat-key="deny-rate">
|
|
62
|
+
<span data-pyric-stat-label="">deny rate</span>
|
|
63
|
+
<span data-pyric-stat-value="">
|
|
64
|
+
{(stats.denyRate * 100).toFixed(0)}%
|
|
65
|
+
</span>
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
<BucketList label="method" buckets={stats.byMethod} />
|
|
69
|
+
<BucketList label="origin" buckets={stats.byOrigin} />
|
|
70
|
+
<BucketList label="path" buckets={stats.byPath} />
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|