@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,289 @@
|
|
|
1
|
+
import { useId, useState, type ReactNode } from 'react';
|
|
2
|
+
import { useTrafficBuckets } from '../hooks/useTrafficBuckets.js';
|
|
3
|
+
import type {
|
|
4
|
+
TimeWindow,
|
|
5
|
+
TrafficBucket,
|
|
6
|
+
UseTrafficBucketsResult,
|
|
7
|
+
} from '../hooks/useTrafficBuckets.js';
|
|
8
|
+
import type { TrafficEvent } from '../types.js';
|
|
9
|
+
|
|
10
|
+
export interface TrafficTimelineProps {
|
|
11
|
+
/**
|
|
12
|
+
* Raw traffic events to bucket. Ignored when `buckets` is supplied
|
|
13
|
+
* (the consumer pre-bucketed). Exactly one of `events` / `buckets`
|
|
14
|
+
* should drive the histogram.
|
|
15
|
+
*/
|
|
16
|
+
events?: TrafficEvent[];
|
|
17
|
+
/**
|
|
18
|
+
* Pre-bucketed counts — the escape hatch when the caller already
|
|
19
|
+
* ran {@link useTrafficBuckets} (or `bucketTraffic`) upstream, e.g.
|
|
20
|
+
* to share one bucketing pass across the timeline + a stats header.
|
|
21
|
+
* Takes precedence over `events`.
|
|
22
|
+
*/
|
|
23
|
+
buckets?: UseTrafficBucketsResult;
|
|
24
|
+
/** The time range the histogram spans. */
|
|
25
|
+
window: TimeWindow;
|
|
26
|
+
/** Number of bars. Default 30. Only used on the `events` path. */
|
|
27
|
+
bucketCount?: number;
|
|
28
|
+
/**
|
|
29
|
+
* A brushed sub-range (`[start, end)`) drawn as an overlay over the
|
|
30
|
+
* bars. Position is derived from where it falls inside `window`, so
|
|
31
|
+
* a partly-out-of-window brush clamps to the chart edges.
|
|
32
|
+
*/
|
|
33
|
+
brush?: TimeWindow;
|
|
34
|
+
/**
|
|
35
|
+
* Fired when a bar inside the brush region is clicked-through — the
|
|
36
|
+
* component itself is presentation-agnostic about drag, so the
|
|
37
|
+
* primary brush gesture is owned by the consumer. As a built-in
|
|
38
|
+
* affordance, clicking a bucket calls this with a one-bucket-wide
|
|
39
|
+
* window so a bare consumer still gets a working selection.
|
|
40
|
+
*/
|
|
41
|
+
onBrush?: (window: TimeWindow) => void;
|
|
42
|
+
/**
|
|
43
|
+
* Direct annotation shown while a bucket is hovered or keyboard-focused.
|
|
44
|
+
* The timeline owns preview state and positioning; the consumer owns the
|
|
45
|
+
* words and number formatting.
|
|
46
|
+
*/
|
|
47
|
+
renderBucketSummary?: (bucket: TrafficBucket) => ReactNode;
|
|
48
|
+
/**
|
|
49
|
+
* Where the live edge marker sits, in epoch-ms. Defaults to
|
|
50
|
+
* `window.end` (the right edge = "now"). Omit / pass `null` to hide
|
|
51
|
+
* the marker entirely (e.g. a frozen, non-live window).
|
|
52
|
+
*/
|
|
53
|
+
liveAt?: number | null;
|
|
54
|
+
/**
|
|
55
|
+
* Header slot — title, deny summary, live label. Rendered above the
|
|
56
|
+
* bars inside `[data-pyric-timeline-header]`. The mock puts
|
|
57
|
+
* "142 requests · 16 denied · live" here.
|
|
58
|
+
*/
|
|
59
|
+
header?: ReactNode;
|
|
60
|
+
/**
|
|
61
|
+
* Axis slot — tick labels below the bars inside
|
|
62
|
+
* `[data-pyric-timeline-axis]`. The mock puts "14m ago · 7m · now"
|
|
63
|
+
* here. Receives the resolved window so labels can be derived.
|
|
64
|
+
*/
|
|
65
|
+
axis?: ReactNode | ((window: TimeWindow) => ReactNode);
|
|
66
|
+
emptyState?: ReactNode;
|
|
67
|
+
className?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Clamp a 0..1 fraction. */
|
|
71
|
+
function clamp01(n: number): number {
|
|
72
|
+
if (n < 0) return 0;
|
|
73
|
+
if (n > 1) return 1;
|
|
74
|
+
return n;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Fraction (0..1) of `t` across `[window.start, window.end)`. */
|
|
78
|
+
function fraction(t: number, window: TimeWindow): number {
|
|
79
|
+
const span = window.end - window.start;
|
|
80
|
+
if (span <= 0) return 0;
|
|
81
|
+
return clamp01((t - window.start) / span);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Headless volume-over-time histogram — the time axis for the traffic
|
|
86
|
+
* lens. Buckets events into N bars; each bar stacks denies (dark, at
|
|
87
|
+
* the base) under the allow remainder, matching the Studio mock.
|
|
88
|
+
*
|
|
89
|
+
* Per-bucket styling channels (on `[data-pyric-bucket]`):
|
|
90
|
+
* - `data-pyric-bucket-count` / `-denies` — raw integers.
|
|
91
|
+
* - `--pyric-bucket-h` — full bar height, 0..1 of the tallest bucket.
|
|
92
|
+
* - `--pyric-bucket-deny-h` — deny sub-stack height, same scale.
|
|
93
|
+
* The deny segment (`[data-pyric-bucket-deny]`) and allow segment
|
|
94
|
+
* (`[data-pyric-bucket-allow]`) are separate children so the consumer
|
|
95
|
+
* colors them independently.
|
|
96
|
+
*
|
|
97
|
+
* A `[data-pyric-brush]` overlay marks a sub-range via
|
|
98
|
+
* `--pyric-brush-left` / `--pyric-brush-right` (0..1 fractions). The
|
|
99
|
+
* `[data-pyric-live]` edge marker sits at `--pyric-live-x`.
|
|
100
|
+
*
|
|
101
|
+
* Styling hooks: `[data-pyric-ui="traffic-timeline"]`,
|
|
102
|
+
* `[data-pyric-timeline-header]`, `[data-pyric-timeline-bars]`,
|
|
103
|
+
* `[data-pyric-bucket]` (with `data-pyric-bucket-index`,
|
|
104
|
+
* `data-pyric-has-denies`, `data-pyric-bucket-selected`),
|
|
105
|
+
* `[data-pyric-bucket-summary]`, `[data-pyric-brush]`, `[data-pyric-live]`,
|
|
106
|
+
* `[data-pyric-timeline-axis]`.
|
|
107
|
+
*/
|
|
108
|
+
export function TrafficTimeline({
|
|
109
|
+
events,
|
|
110
|
+
buckets: prebucketed,
|
|
111
|
+
window,
|
|
112
|
+
bucketCount = 30,
|
|
113
|
+
brush,
|
|
114
|
+
onBrush,
|
|
115
|
+
renderBucketSummary,
|
|
116
|
+
liveAt,
|
|
117
|
+
header,
|
|
118
|
+
axis,
|
|
119
|
+
emptyState,
|
|
120
|
+
className,
|
|
121
|
+
}: TrafficTimelineProps) {
|
|
122
|
+
const summaryId = useId();
|
|
123
|
+
const [hoveredBucket, setHoveredBucket] = useState<number | null>(null);
|
|
124
|
+
const [focusedBucket, setFocusedBucket] = useState<number | null>(null);
|
|
125
|
+
// Hook is always called (rules of hooks); its result is discarded
|
|
126
|
+
// when the caller pre-bucketed.
|
|
127
|
+
const computed = useTrafficBuckets({
|
|
128
|
+
events: events ?? [],
|
|
129
|
+
window,
|
|
130
|
+
bucketCount,
|
|
131
|
+
});
|
|
132
|
+
const result = prebucketed ?? computed;
|
|
133
|
+
const { buckets, total } = result;
|
|
134
|
+
const activeBucketIndex = hoveredBucket ?? focusedBucket;
|
|
135
|
+
const activeBucket =
|
|
136
|
+
activeBucketIndex === null ? null : (buckets.find((bucket) => bucket.index === activeBucketIndex) ?? null);
|
|
137
|
+
const activeBucketSelected =
|
|
138
|
+
activeBucket !== null && brush?.start === activeBucket.start && brush.end === activeBucket.end;
|
|
139
|
+
|
|
140
|
+
if (buckets.length === 0) {
|
|
141
|
+
return (
|
|
142
|
+
<div
|
|
143
|
+
className={className}
|
|
144
|
+
data-pyric-ui="traffic-timeline"
|
|
145
|
+
data-pyric-empty=""
|
|
146
|
+
>
|
|
147
|
+
{emptyState}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const isEmpty = total === 0;
|
|
153
|
+
const livePoint = liveAt === undefined ? window.end : liveAt;
|
|
154
|
+
const liveX = livePoint == null ? null : fraction(livePoint, window);
|
|
155
|
+
|
|
156
|
+
const resolvedAxis = typeof axis === 'function' ? axis(window) : axis;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
className={className}
|
|
161
|
+
data-pyric-ui="traffic-timeline"
|
|
162
|
+
data-pyric-empty={isEmpty ? '' : undefined}
|
|
163
|
+
>
|
|
164
|
+
{header !== undefined && (
|
|
165
|
+
<div data-pyric-timeline-header="">{header}</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
<div data-pyric-timeline-chart="">
|
|
169
|
+
<div
|
|
170
|
+
data-pyric-timeline-bars=""
|
|
171
|
+
role={onBrush ? 'group' : 'img'}
|
|
172
|
+
aria-label="request volume over time"
|
|
173
|
+
>
|
|
174
|
+
{buckets.map((bucket) => (
|
|
175
|
+
<Bar
|
|
176
|
+
key={bucket.index}
|
|
177
|
+
bucket={bucket}
|
|
178
|
+
window={window}
|
|
179
|
+
onBrush={onBrush}
|
|
180
|
+
onHover={renderBucketSummary ? setHoveredBucket : undefined}
|
|
181
|
+
onFocus={renderBucketSummary ? setFocusedBucket : undefined}
|
|
182
|
+
summaryId={
|
|
183
|
+
renderBucketSummary && !activeBucketSelected && activeBucket?.index === bucket.index
|
|
184
|
+
? summaryId
|
|
185
|
+
: undefined
|
|
186
|
+
}
|
|
187
|
+
selected={brush?.start === bucket.start && brush.end === bucket.end}
|
|
188
|
+
/>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{activeBucket && renderBucketSummary && !activeBucketSelected ? (
|
|
193
|
+
<div
|
|
194
|
+
id={summaryId}
|
|
195
|
+
role="tooltip"
|
|
196
|
+
data-pyric-bucket-summary=""
|
|
197
|
+
style={
|
|
198
|
+
{
|
|
199
|
+
'--pyric-bucket-summary-x': (activeBucket.index + 0.5) / Math.max(buckets.length, 1),
|
|
200
|
+
} as React.CSSProperties
|
|
201
|
+
}
|
|
202
|
+
>
|
|
203
|
+
{renderBucketSummary(activeBucket)}
|
|
204
|
+
</div>
|
|
205
|
+
) : null}
|
|
206
|
+
|
|
207
|
+
{brush && (
|
|
208
|
+
<div
|
|
209
|
+
data-pyric-brush=""
|
|
210
|
+
aria-hidden="true"
|
|
211
|
+
style={
|
|
212
|
+
{
|
|
213
|
+
'--pyric-brush-left': fraction(brush.start, window),
|
|
214
|
+
'--pyric-brush-right': 1 - fraction(brush.end, window),
|
|
215
|
+
} as React.CSSProperties
|
|
216
|
+
}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{liveX != null && (
|
|
221
|
+
<div
|
|
222
|
+
data-pyric-live=""
|
|
223
|
+
aria-hidden="true"
|
|
224
|
+
style={{ '--pyric-live-x': liveX } as React.CSSProperties}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{resolvedAxis !== undefined && resolvedAxis !== null && (
|
|
230
|
+
<div data-pyric-timeline-axis="">{resolvedAxis}</div>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function Bar({
|
|
237
|
+
bucket,
|
|
238
|
+
window,
|
|
239
|
+
onBrush,
|
|
240
|
+
onHover,
|
|
241
|
+
onFocus,
|
|
242
|
+
summaryId,
|
|
243
|
+
selected,
|
|
244
|
+
}: {
|
|
245
|
+
bucket: TrafficBucket;
|
|
246
|
+
window: TimeWindow;
|
|
247
|
+
onBrush?: (window: TimeWindow) => void;
|
|
248
|
+
onHover?: (index: number | null) => void;
|
|
249
|
+
onFocus?: (index: number | null) => void;
|
|
250
|
+
summaryId?: string;
|
|
251
|
+
selected: boolean;
|
|
252
|
+
}) {
|
|
253
|
+
const interactive = onBrush !== undefined;
|
|
254
|
+
const requestLabel = `${bucket.count} ${bucket.count === 1 ? 'request' : 'requests'}`;
|
|
255
|
+
const denyLabel = `${bucket.denies} denied`;
|
|
256
|
+
return (
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
data-pyric-bucket=""
|
|
260
|
+
data-pyric-bucket-index={bucket.index}
|
|
261
|
+
data-pyric-bucket-count={bucket.count}
|
|
262
|
+
data-pyric-bucket-denies={bucket.denies}
|
|
263
|
+
data-pyric-has-denies={bucket.denies > 0 ? '' : undefined}
|
|
264
|
+
data-pyric-bucket-selected={selected ? '' : undefined}
|
|
265
|
+
disabled={!interactive}
|
|
266
|
+
aria-label={`${requestLabel}, ${denyLabel}`}
|
|
267
|
+
aria-describedby={summaryId}
|
|
268
|
+
aria-pressed={interactive ? selected : undefined}
|
|
269
|
+
onPointerEnter={onHover ? () => onHover(bucket.index) : undefined}
|
|
270
|
+
onPointerLeave={onHover ? () => onHover(null) : undefined}
|
|
271
|
+
onFocus={onFocus ? () => onFocus(bucket.index) : undefined}
|
|
272
|
+
onBlur={onFocus ? () => onFocus(null) : undefined}
|
|
273
|
+
onClick={
|
|
274
|
+
interactive
|
|
275
|
+
? () => onBrush?.({ start: bucket.start, end: bucket.end })
|
|
276
|
+
: undefined
|
|
277
|
+
}
|
|
278
|
+
style={
|
|
279
|
+
{
|
|
280
|
+
'--pyric-bucket-h': bucket.heightRatio,
|
|
281
|
+
'--pyric-bucket-deny-h': bucket.denyHeightRatio,
|
|
282
|
+
} as React.CSSProperties
|
|
283
|
+
}
|
|
284
|
+
>
|
|
285
|
+
<span data-pyric-bucket-allow="" aria-hidden="true" />
|
|
286
|
+
<span data-pyric-bucket-deny="" aria-hidden="true" />
|
|
287
|
+
</button>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `HH:MM:SS` timestamp formatter. Components accept a
|
|
3
|
+
* `formatTime` prop to override — this is just the fallback so the
|
|
4
|
+
* library has no hard locale dependency.
|
|
5
|
+
*/
|
|
6
|
+
export function defaultFormatTime(at: number): string {
|
|
7
|
+
const d = new Date(at);
|
|
8
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
9
|
+
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Classifies a simulator debug line as a deny / allow / neutral
|
|
14
|
+
* verdict so consumers can tint reason rows via
|
|
15
|
+
* `[data-pyric-reason-verdict="…"]`.
|
|
16
|
+
*/
|
|
17
|
+
export function reasonVerdict(reason: string): 'deny' | 'allow' | 'neutral' {
|
|
18
|
+
const lower = reason.toLowerCase();
|
|
19
|
+
if (lower.includes('deny')) return 'deny';
|
|
20
|
+
if (lower.includes('allow')) return 'allow';
|
|
21
|
+
return 'neutral';
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { TrafficRow, type TrafficRowProps } from './TrafficRow.js';
|
|
2
|
+
export { TrafficLog, type TrafficLogProps } from './TrafficLog.js';
|
|
3
|
+
export {
|
|
4
|
+
TrafficGroupRow,
|
|
5
|
+
type TrafficGroupRowProps,
|
|
6
|
+
} from './TrafficGroupRow.js';
|
|
7
|
+
export { TrafficDetail, type TrafficDetailProps } from './TrafficDetail.js';
|
|
8
|
+
export { RuleHeatmap, type RuleHeatmapProps } from './RuleHeatmap.js';
|
|
9
|
+
export { TrafficStats, type TrafficStatsProps } from './TrafficStats.js';
|
|
10
|
+
export {
|
|
11
|
+
TrafficTimeline,
|
|
12
|
+
type TrafficTimelineProps,
|
|
13
|
+
} from './TrafficTimeline.js';
|
|
14
|
+
export { defaultFormatTime, reasonVerdict } from './format.js';
|
|
15
|
+
export {
|
|
16
|
+
TrafficMetricCards,
|
|
17
|
+
type TrafficMetricCardsProps,
|
|
18
|
+
} from './TrafficMetricCards.js';
|
|
19
|
+
export {
|
|
20
|
+
TrafficLineChart,
|
|
21
|
+
type TrafficLineChartProps,
|
|
22
|
+
} from './TrafficLineChart.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export {
|
|
2
|
+
useTrafficMonitor,
|
|
3
|
+
type UseTrafficMonitorOptions,
|
|
4
|
+
type UseTrafficMonitorResult,
|
|
5
|
+
type TrafficCounts,
|
|
6
|
+
} from './useTrafficMonitor.js';
|
|
7
|
+
export {
|
|
8
|
+
useTrafficFilter,
|
|
9
|
+
type UseTrafficFilterOptions,
|
|
10
|
+
type UseTrafficFilterResult,
|
|
11
|
+
type TrafficFilterState,
|
|
12
|
+
type TrafficOriginFilter,
|
|
13
|
+
type TrafficResultFilter,
|
|
14
|
+
} from './useTrafficFilter.js';
|
|
15
|
+
export {
|
|
16
|
+
useRuleHeatmap,
|
|
17
|
+
type UseRuleHeatmapOptions,
|
|
18
|
+
type UseRuleHeatmapResult,
|
|
19
|
+
type RuleHeatmapEntry,
|
|
20
|
+
} from './useRuleHeatmap.js';
|
|
21
|
+
export {
|
|
22
|
+
useTrafficStats,
|
|
23
|
+
type UseTrafficStatsOptions,
|
|
24
|
+
type TrafficStatsSummary,
|
|
25
|
+
type TrafficStatBucket,
|
|
26
|
+
} from './useTrafficStats.js';
|
|
27
|
+
export {
|
|
28
|
+
useTrafficGroups,
|
|
29
|
+
type UseTrafficGroupsOptions,
|
|
30
|
+
type UseTrafficGroupsResult,
|
|
31
|
+
type TrafficLogItem,
|
|
32
|
+
type TrafficGroup,
|
|
33
|
+
type TrafficSingle,
|
|
34
|
+
type TrafficGroupKind,
|
|
35
|
+
} from './useTrafficGroups.js';
|
|
36
|
+
export {
|
|
37
|
+
useTrafficBuckets,
|
|
38
|
+
bucketTraffic,
|
|
39
|
+
type UseTrafficBucketsOptions,
|
|
40
|
+
type UseTrafficBucketsResult,
|
|
41
|
+
type TrafficBucket,
|
|
42
|
+
type TimeWindow,
|
|
43
|
+
} from './useTrafficBuckets.js';
|
|
44
|
+
export {
|
|
45
|
+
useBillableMetrics,
|
|
46
|
+
useRulesMetrics,
|
|
47
|
+
bucketBillableMetrics,
|
|
48
|
+
bucketRulesMetrics,
|
|
49
|
+
classifyBillable,
|
|
50
|
+
classifyRules,
|
|
51
|
+
isAdminEvent,
|
|
52
|
+
BILLABLE_SERIES_DEFS,
|
|
53
|
+
RULES_SERIES_DEFS,
|
|
54
|
+
type UseTrafficMetricsOptions,
|
|
55
|
+
type TrafficMetricsResult,
|
|
56
|
+
type MetricPoint,
|
|
57
|
+
type MetricSeries,
|
|
58
|
+
type BillableSeriesKey,
|
|
59
|
+
type RulesSeriesKey,
|
|
60
|
+
} from './useTrafficMetrics.js';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { TrafficEvent } from '../types.js';
|
|
3
|
+
|
|
4
|
+
export interface RuleHeatmapEntry {
|
|
5
|
+
/** The rule's index in the rules file. */
|
|
6
|
+
ruleIndex: number;
|
|
7
|
+
/** Union of all operations seen matching this rule. */
|
|
8
|
+
operations: string[];
|
|
9
|
+
/** Total events that matched this rule. */
|
|
10
|
+
total: number;
|
|
11
|
+
allows: number;
|
|
12
|
+
denies: number;
|
|
13
|
+
unsupported: number;
|
|
14
|
+
/** `denies / total` — 0 when the rule never denied. */
|
|
15
|
+
denyRatio: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseRuleHeatmapOptions {
|
|
19
|
+
events: TrafficEvent[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseRuleHeatmapResult {
|
|
23
|
+
/** Per-rule rollup, sorted by `total` descending (busiest first),
|
|
24
|
+
* ties broken by `ruleIndex` ascending. */
|
|
25
|
+
entries: RuleHeatmapEntry[];
|
|
26
|
+
/** Events that matched no rule — counted here, not attributed to
|
|
27
|
+
* any entry. */
|
|
28
|
+
unmatchedCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Accumulator {
|
|
32
|
+
ruleIndex: number;
|
|
33
|
+
operations: Set<string>;
|
|
34
|
+
total: number;
|
|
35
|
+
allows: number;
|
|
36
|
+
denies: number;
|
|
37
|
+
unsupported: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Rolls a traffic buffer up by `matchedRule.ruleIndex`: how often
|
|
42
|
+
* each rule fired, and how that split across allow / deny /
|
|
43
|
+
* unsupported. Pure derivation — feed it the filtered or full
|
|
44
|
+
* event list depending on what the heatmap should reflect.
|
|
45
|
+
*/
|
|
46
|
+
export function useRuleHeatmap({
|
|
47
|
+
events,
|
|
48
|
+
}: UseRuleHeatmapOptions): UseRuleHeatmapResult {
|
|
49
|
+
return useMemo(() => {
|
|
50
|
+
const byRule = new Map<number, Accumulator>();
|
|
51
|
+
let unmatchedCount = 0;
|
|
52
|
+
|
|
53
|
+
for (const event of events) {
|
|
54
|
+
if (!event.matchedRule) {
|
|
55
|
+
unmatchedCount++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const { ruleIndex, operations } = event.matchedRule;
|
|
59
|
+
let acc = byRule.get(ruleIndex);
|
|
60
|
+
if (!acc) {
|
|
61
|
+
acc = {
|
|
62
|
+
ruleIndex,
|
|
63
|
+
operations: new Set(),
|
|
64
|
+
total: 0,
|
|
65
|
+
allows: 0,
|
|
66
|
+
denies: 0,
|
|
67
|
+
unsupported: 0,
|
|
68
|
+
};
|
|
69
|
+
byRule.set(ruleIndex, acc);
|
|
70
|
+
}
|
|
71
|
+
for (const op of operations) acc.operations.add(op);
|
|
72
|
+
acc.total++;
|
|
73
|
+
if (event.result === 'allow') acc.allows++;
|
|
74
|
+
else if (event.result === 'deny') acc.denies++;
|
|
75
|
+
else acc.unsupported++;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const entries: RuleHeatmapEntry[] = [...byRule.values()]
|
|
79
|
+
.map((acc) => ({
|
|
80
|
+
ruleIndex: acc.ruleIndex,
|
|
81
|
+
operations: [...acc.operations],
|
|
82
|
+
total: acc.total,
|
|
83
|
+
allows: acc.allows,
|
|
84
|
+
denies: acc.denies,
|
|
85
|
+
unsupported: acc.unsupported,
|
|
86
|
+
denyRatio: acc.total === 0 ? 0 : acc.denies / acc.total,
|
|
87
|
+
}))
|
|
88
|
+
.sort((a, b) => b.total - a.total || a.ruleIndex - b.ruleIndex);
|
|
89
|
+
|
|
90
|
+
return { entries, unmatchedCount };
|
|
91
|
+
}, [events]);
|
|
92
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { TrafficEvent } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A half-open time window `[start, end)` in epoch-ms. The timeline
|
|
6
|
+
* buckets events whose `at` falls inside it; events outside are
|
|
7
|
+
* dropped from the histogram (but still counted in `outOfWindow`).
|
|
8
|
+
*/
|
|
9
|
+
export interface TimeWindow {
|
|
10
|
+
start: number;
|
|
11
|
+
end: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TrafficBucket {
|
|
15
|
+
/** 0-based bucket index, left (oldest) to right (newest). */
|
|
16
|
+
index: number;
|
|
17
|
+
/** Half-open bounds of this bucket `[start, end)` in epoch-ms. */
|
|
18
|
+
start: number;
|
|
19
|
+
end: number;
|
|
20
|
+
/** Total events that fell in this bucket. */
|
|
21
|
+
count: number;
|
|
22
|
+
/** How many of `count` were denied. */
|
|
23
|
+
denies: number;
|
|
24
|
+
/** `count - denies` — allowed + unsupported. The "non-deny" stack. */
|
|
25
|
+
allows: number;
|
|
26
|
+
/**
|
|
27
|
+
* `count / maxCount` across all buckets — 0..1. The full bar
|
|
28
|
+
* height as a fraction of the tallest bucket. Drives
|
|
29
|
+
* `--pyric-bucket-h`.
|
|
30
|
+
*/
|
|
31
|
+
heightRatio: number;
|
|
32
|
+
/**
|
|
33
|
+
* `denies / maxCount` — 0..1. The deny sub-stack height as a
|
|
34
|
+
* fraction of the tallest bucket, so the deny segment is drawn to
|
|
35
|
+
* the same scale as the full bar. Drives `--pyric-bucket-deny-h`.
|
|
36
|
+
*/
|
|
37
|
+
denyHeightRatio: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface UseTrafficBucketsOptions {
|
|
41
|
+
events: TrafficEvent[];
|
|
42
|
+
/** The time range to bucket over. */
|
|
43
|
+
window: TimeWindow;
|
|
44
|
+
/** Number of buckets to divide the window into. Default 30. */
|
|
45
|
+
bucketCount?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface UseTrafficBucketsResult {
|
|
49
|
+
buckets: TrafficBucket[];
|
|
50
|
+
/** Sum of `count` across buckets (events inside the window). */
|
|
51
|
+
total: number;
|
|
52
|
+
/** Sum of `denies` across buckets. */
|
|
53
|
+
denies: number;
|
|
54
|
+
/** The largest single-bucket `count` — the height-ratio divisor. */
|
|
55
|
+
maxCount: number;
|
|
56
|
+
/** Events whose `at` fell outside `[window.start, window.end)`. */
|
|
57
|
+
outOfWindow: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const EMPTY_RESULT: UseTrafficBucketsResult = {
|
|
61
|
+
buckets: [],
|
|
62
|
+
total: 0,
|
|
63
|
+
denies: 0,
|
|
64
|
+
maxCount: 0,
|
|
65
|
+
outOfWindow: 0,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Buckets a traffic buffer into `bucketCount` equal time slices over
|
|
70
|
+
* `window`, counting total + denied events per slice. Pure
|
|
71
|
+
* derivation — the histogram component renders the result, this hook
|
|
72
|
+
* (and `bucketTraffic` under it) owns the math.
|
|
73
|
+
*
|
|
74
|
+
* Each bucket carries a `heightRatio` and `denyHeightRatio`
|
|
75
|
+
* (0..1, scaled to the tallest bucket) so the consumer can map them
|
|
76
|
+
* straight onto a bar height without re-finding the max.
|
|
77
|
+
*/
|
|
78
|
+
export function useTrafficBuckets({
|
|
79
|
+
events,
|
|
80
|
+
window,
|
|
81
|
+
bucketCount = 30,
|
|
82
|
+
}: UseTrafficBucketsOptions): UseTrafficBucketsResult {
|
|
83
|
+
return useMemo(
|
|
84
|
+
() => bucketTraffic(events, window, bucketCount),
|
|
85
|
+
[events, window.start, window.end, bucketCount],
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The pure bucketing kernel behind {@link useTrafficBuckets} — usable
|
|
91
|
+
* outside React. Returns an empty result for a non-positive
|
|
92
|
+
* `bucketCount` or a zero/negative-width window.
|
|
93
|
+
*/
|
|
94
|
+
export function bucketTraffic(
|
|
95
|
+
events: TrafficEvent[],
|
|
96
|
+
window: TimeWindow,
|
|
97
|
+
bucketCount = 30,
|
|
98
|
+
): UseTrafficBucketsResult {
|
|
99
|
+
const span = window.end - window.start;
|
|
100
|
+
if (bucketCount <= 0 || span <= 0) return EMPTY_RESULT;
|
|
101
|
+
|
|
102
|
+
const width = span / bucketCount;
|
|
103
|
+
const counts = new Array<number>(bucketCount).fill(0);
|
|
104
|
+
const denyCounts = new Array<number>(bucketCount).fill(0);
|
|
105
|
+
|
|
106
|
+
let total = 0;
|
|
107
|
+
let denies = 0;
|
|
108
|
+
let outOfWindow = 0;
|
|
109
|
+
|
|
110
|
+
for (const event of events) {
|
|
111
|
+
const at = event.at;
|
|
112
|
+
if (at < window.start || at >= window.end) {
|
|
113
|
+
outOfWindow++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Floor into a bucket; clamp the right edge so an `at` exactly at
|
|
117
|
+
// `window.end - epsilon` never spills past the last bucket.
|
|
118
|
+
let i = Math.floor((at - window.start) / width);
|
|
119
|
+
if (i >= bucketCount) i = bucketCount - 1;
|
|
120
|
+
counts[i]++;
|
|
121
|
+
total++;
|
|
122
|
+
if (event.result === 'deny') {
|
|
123
|
+
denyCounts[i]++;
|
|
124
|
+
denies++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let maxCount = 0;
|
|
129
|
+
for (const c of counts) if (c > maxCount) maxCount = c;
|
|
130
|
+
|
|
131
|
+
const buckets: TrafficBucket[] = counts.map((count, index) => {
|
|
132
|
+
const denyCount = denyCounts[index];
|
|
133
|
+
return {
|
|
134
|
+
index,
|
|
135
|
+
start: window.start + index * width,
|
|
136
|
+
end: window.start + (index + 1) * width,
|
|
137
|
+
count,
|
|
138
|
+
denies: denyCount,
|
|
139
|
+
allows: count - denyCount,
|
|
140
|
+
heightRatio: maxCount === 0 ? 0 : count / maxCount,
|
|
141
|
+
denyHeightRatio: maxCount === 0 ? 0 : denyCount / maxCount,
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return { buckets, total, denies, maxCount, outOfWindow };
|
|
146
|
+
}
|