@bimetal/devtools-react 0.16.5
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/LICENSE +129 -0
- package/README.md +61 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/BridgesBody.d.ts +27 -0
- package/dist/BridgesBody.d.ts.map +1 -0
- package/dist/BridgesBody.js +154 -0
- package/dist/BridgesBody.js.map +1 -0
- package/dist/CausationBody.d.ts +30 -0
- package/dist/CausationBody.d.ts.map +1 -0
- package/dist/CausationBody.js +149 -0
- package/dist/CausationBody.js.map +1 -0
- package/dist/DevtoolsRegistryContext.d.ts +14 -0
- package/dist/DevtoolsRegistryContext.d.ts.map +1 -0
- package/dist/DevtoolsRegistryContext.js +14 -0
- package/dist/DevtoolsRegistryContext.js.map +1 -0
- package/dist/DevtoolsRegistryProvider.d.ts +16 -0
- package/dist/DevtoolsRegistryProvider.d.ts.map +1 -0
- package/dist/DevtoolsRegistryProvider.js +14 -0
- package/dist/DevtoolsRegistryProvider.js.map +1 -0
- package/dist/EventInspectorPanel.d.ts +27 -0
- package/dist/EventInspectorPanel.d.ts.map +1 -0
- package/dist/EventInspectorPanel.js +669 -0
- package/dist/EventInspectorPanel.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/inspector-shared.d.ts +52 -0
- package/dist/inspector-shared.d.ts.map +1 -0
- package/dist/inspector-shared.js +168 -0
- package/dist/inspector-shared.js.map +1 -0
- package/dist/useDevtoolsRegistry.d.ts +20 -0
- package/dist/useDevtoolsRegistry.d.ts.map +1 -0
- package/dist/useDevtoolsRegistry.js +37 -0
- package/dist/useDevtoolsRegistry.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Event-Inspector — Vollseiten-Variant (v0.14.0, in `@bimetal/devtools-react`
|
|
4
|
+
* extrahiert in v0.16.5).
|
|
5
|
+
*
|
|
6
|
+
* UX-Vorbild laut Architektur-Doc: EDA-Lab / KurrentDB-UI.
|
|
7
|
+
* Tabelle statt Cards. ID-Stubs (8-Zeichen) statt Volltext.
|
|
8
|
+
* Causation/Correlation als first-class Spalten.
|
|
9
|
+
*
|
|
10
|
+
* Ehrlichkeits-Grundsatz: causationId zeigt im Calendar-DomainStore-Flow
|
|
11
|
+
* auf eine Command-ID, nicht auf ein vorausgehendes Event. Die Tabelle
|
|
12
|
+
* zeigt die rohe ID — keine Fake-Causation-Tree-Verlinkung.
|
|
13
|
+
*
|
|
14
|
+
* **Registry-Resolution (v0.16.5 Pilot-Schärfung):** Prop > Context >
|
|
15
|
+
* Throw. Kein impliziter Singleton-Fallback. Siehe
|
|
16
|
+
* `16-devtools-react-architecture.md` Leitplanke 2.
|
|
17
|
+
*/
|
|
18
|
+
import { useState, useSyncExternalStore, useMemo, useEffect } from 'react';
|
|
19
|
+
import { exportSnapshot, serializeSnapshot } from '@bimetal/devtools';
|
|
20
|
+
import { CausationBody } from './CausationBody.js';
|
|
21
|
+
import { BridgesBody } from './BridgesBody.js';
|
|
22
|
+
import { useResolvedRegistry } from './useDevtoolsRegistry.js';
|
|
23
|
+
import { DevtoolsRegistryContext } from './DevtoolsRegistryContext.js';
|
|
24
|
+
import { EventDetail, MetaPlainRow, MetaRow, Section, detailColumnStyle, detailHeaderStyle, detailInnerStyle, detailMetaGridStyle, emptyDetailStyle, emptyStyle, formatTimeWithMs, inlineCodeStyle, preStyle, safeStringify, stub, tableScrollStyle, twoColStyle, } from './inspector-shared.js';
|
|
25
|
+
export function EventInspectorPanel({ registry: registryProp } = {}) {
|
|
26
|
+
const registry = useResolvedRegistry(registryProp);
|
|
27
|
+
const [activeTab, setActiveTab] = useState('events');
|
|
28
|
+
const [selectedSeq, setSelectedSeq] = useState(null);
|
|
29
|
+
const [typeFilter, setTypeFilter] = useState('');
|
|
30
|
+
const [streamFilter, setStreamFilter] = useState('');
|
|
31
|
+
const [storeFilter, setStoreFilter] = useState('');
|
|
32
|
+
const [minSeq, setMinSeq] = useState(0);
|
|
33
|
+
const [snapshotWhenPaused, setSnapshotWhenPaused] = useState(null);
|
|
34
|
+
const liveState = useSyncExternalStore(registry.subscribe, () => registry.state, () => registry.state);
|
|
35
|
+
const state = snapshotWhenPaused ?? liveState;
|
|
36
|
+
const paused = snapshotWhenPaused !== null;
|
|
37
|
+
const visible = useMemo(() => {
|
|
38
|
+
const tf = typeFilter.toLowerCase();
|
|
39
|
+
const sf = streamFilter.toLowerCase();
|
|
40
|
+
return state.events.filter(e => {
|
|
41
|
+
if (e.seq < minSeq)
|
|
42
|
+
return false;
|
|
43
|
+
if (storeFilter && e.storeId !== storeFilter)
|
|
44
|
+
return false;
|
|
45
|
+
if (tf && !e.event.type.toLowerCase().includes(tf))
|
|
46
|
+
return false;
|
|
47
|
+
if (sf && !e.streamId.toLowerCase().includes(sf))
|
|
48
|
+
return false;
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
}, [state.events, minSeq, storeFilter, typeFilter, streamFilter]);
|
|
52
|
+
const streamCount = useMemo(() => {
|
|
53
|
+
const set = new Set();
|
|
54
|
+
for (const e of state.events) {
|
|
55
|
+
if (e.seq >= minSeq)
|
|
56
|
+
set.add(`${e.storeId}::${e.streamId}`);
|
|
57
|
+
}
|
|
58
|
+
return set.size;
|
|
59
|
+
}, [state.events, minSeq]);
|
|
60
|
+
const selected = state.events.find(e => e.seq === selectedSeq) ?? null;
|
|
61
|
+
function togglePause() {
|
|
62
|
+
setSnapshotWhenPaused(prev => (prev === null ? liveState : null));
|
|
63
|
+
}
|
|
64
|
+
function clear() {
|
|
65
|
+
const maxSeq = state.events.length > 0 ? state.events[state.events.length - 1].seq : 0;
|
|
66
|
+
setMinSeq(maxSeq + 1);
|
|
67
|
+
setSelectedSeq(null);
|
|
68
|
+
}
|
|
69
|
+
// Snapshot-Export (v0.16.6 Sub 2). Date.now() läuft am UI-Layer —
|
|
70
|
+
// exportSnapshot bleibt deterministisch (Pilot-Schärfung Sub 1).
|
|
71
|
+
// Exportiert den aktuellen `state` (= paused-Snapshot wenn pausiert,
|
|
72
|
+
// sonst Live-Snapshot). Filename mit saniertem ISO-Datum
|
|
73
|
+
// (Doppelpunkt/Punkt → Bindestrich; Windows-Datei-System-freundlich).
|
|
74
|
+
function exportAndDownload() {
|
|
75
|
+
const exportedAt = Date.now();
|
|
76
|
+
const snap = exportSnapshot(state, { exportedAt });
|
|
77
|
+
const json = serializeSnapshot(snap);
|
|
78
|
+
const iso = new Date(exportedAt).toISOString();
|
|
79
|
+
const filename = `bimetal-devtools-snapshot-${iso.replace(/[:.]/g, '-')}.json`;
|
|
80
|
+
triggerDownload(filename, json);
|
|
81
|
+
}
|
|
82
|
+
const visibleCountInWindow = useMemo(() => state.events.filter(e => e.seq >= minSeq).length, [state.events, minSeq]);
|
|
83
|
+
// Causation-Tab respektiert den Clear-Button-Vertrag wie der Events-Tab:
|
|
84
|
+
// nach Clear (= minSeq nach vorne geschoben) sollen die alten Events
|
|
85
|
+
// auch im Forest verschwinden. Andere Filter (type/stream/store) bleiben
|
|
86
|
+
// bewusst draußen — der Forest soll die Causation-Topologie zeigen, nicht
|
|
87
|
+
// eine gefilterte Teilsicht.
|
|
88
|
+
const causationEvents = useMemo(() => state.events.filter(e => e.seq >= minSeq), [state.events, minSeq]);
|
|
89
|
+
// v0.15.5 + v0.16.0: Tab-Bar wird zur Laufzeit aufgebaut. Tabs werden
|
|
90
|
+
// einzeln sichtbar, sobald ihr Datenfeld nicht leer ist (überlebt
|
|
91
|
+
// Variant-Switch via rolling-window oder Registry-Stand).
|
|
92
|
+
// Bridges-Tab respektiert Clear-Button-Vertrag wie Events- und Causation-
|
|
93
|
+
// Tab: nach Clear (minSeq nach vorne) verschwinden alte Bridge-Records.
|
|
94
|
+
// Type-/Stream-/Store-Filter bleiben bewusst draußen — der Bridge-Tab
|
|
95
|
+
// zeigt Transport-Topologie, keine Event-Filter-Teilsicht.
|
|
96
|
+
const bridgeRecords = useMemo(() => state.bridgeRecords.filter(r => r.seq >= minSeq), [state.bridgeRecords, minSeq]);
|
|
97
|
+
const showSyncTab = state.syncTransports.length > 0 || state.syncRecords.length > 0;
|
|
98
|
+
const showStateTab = state.replayTargets.length > 0;
|
|
99
|
+
const showCausationTab = causationEvents.length > 0;
|
|
100
|
+
const showBridgesTab = state.bridges.length > 0 || bridgeRecords.length > 0;
|
|
101
|
+
const showTabs = showSyncTab || showStateTab || showCausationTab || showBridgesTab;
|
|
102
|
+
// Wenn der gerade aktive Tab nicht mehr verfügbar ist (Variant-Wechsel
|
|
103
|
+
// hat Targets/Transports abgemeldet), fall back auf Events.
|
|
104
|
+
const effectiveTab = (activeTab === 'sync' && !showSyncTab) ||
|
|
105
|
+
(activeTab === 'state' && !showStateTab) ||
|
|
106
|
+
(activeTab === 'causation' && !showCausationTab) ||
|
|
107
|
+
(activeTab === 'bridges' && !showBridgesTab)
|
|
108
|
+
? 'events'
|
|
109
|
+
: activeTab;
|
|
110
|
+
// Resolved Registry intern als Context bereitstellen, damit interne
|
|
111
|
+
// Tab-Bodies und zukünftige Sub-Komponenten useDevtoolsRegistry() nutzen
|
|
112
|
+
// können, auch wenn der Konsument nur per Prop konfiguriert hat.
|
|
113
|
+
// Pilot-Schärfung Sub 1: Hook liest ausschließlich Context — der Panel
|
|
114
|
+
// ist die Stelle, die Prop und Context vereinheitlicht.
|
|
115
|
+
return (_jsx(DevtoolsRegistryContext.Provider, { value: registry, children: _jsxs("div", { style: pageStyle, children: [_jsx(PanelHeader, { events: visibleCountInWindow, streams: streamCount, stores: state.eventStores.length, syncTransports: state.syncTransports.length, replayTargets: state.replayTargets.length, filtered: visible.length, paused: paused, onTogglePause: togglePause, onClear: clear, onExport: exportAndDownload }), showTabs && (_jsx(TabBar, { activeTab: effectiveTab, onSelect: setActiveTab, showSync: showSyncTab, showState: showStateTab, showCausation: showCausationTab, showBridges: showBridgesTab })), effectiveTab === 'events' && (_jsxs(_Fragment, { children: [_jsx(FilterBar, { stores: state.eventStores, storeFilter: storeFilter, onStoreFilter: setStoreFilter, typeFilter: typeFilter, onTypeFilter: setTypeFilter, streamFilter: streamFilter, onStreamFilter: setStreamFilter }), _jsx(EventsBody, { state: state, visible: visible, selected: selected, selectedSeq: selectedSeq, onSelect: setSelectedSeq, paused: paused })] })), effectiveTab === 'sync' && (_jsx(SyncBody, { records: state.syncRecords, paused: paused })), effectiveTab === 'state' && (_jsx(StateBody, { targets: state.replayTargets, registry: registry })), effectiveTab === 'causation' && (_jsx(CausationBody, { events: causationEvents })), effectiveTab === 'bridges' && (_jsx(BridgesBody, { bridges: state.bridges, records: bridgeRecords }))] }) }));
|
|
116
|
+
}
|
|
117
|
+
function TabBar({ activeTab, onSelect, showSync, showState, showCausation, showBridges, }) {
|
|
118
|
+
return (_jsxs("div", { style: tabBarStyle, children: [_jsx("button", { onClick: () => onSelect('events'), style: activeTab === 'events' ? tabActiveStyle : tabStyle, children: "Events" }), showSync && (_jsx("button", { onClick: () => onSelect('sync'), style: activeTab === 'sync' ? tabActiveStyle : tabStyle, children: "Sync" })), showState && (_jsx("button", { onClick: () => onSelect('state'), style: activeTab === 'state' ? tabActiveStyle : tabStyle, children: "State" })), showCausation && (_jsx("button", { onClick: () => onSelect('causation'), style: activeTab === 'causation' ? tabActiveStyle : tabStyle, children: "Causation" })), showBridges && (_jsx("button", { onClick: () => onSelect('bridges'), style: activeTab === 'bridges' ? tabActiveStyle : tabStyle, children: "Bridges" }))] }));
|
|
119
|
+
}
|
|
120
|
+
// ── Sync-Tab ──────────────────────────────────────────────────────
|
|
121
|
+
function SyncBody({ records, paused }) {
|
|
122
|
+
const [selectedSeq, setSelectedSeq] = useState(null);
|
|
123
|
+
const selected = records.find(r => r.seq === selectedSeq) ?? null;
|
|
124
|
+
if (records.length === 0) {
|
|
125
|
+
return (_jsxs("div", { style: emptyStyle, children: ["Noch keine Sync-Records. Sobald ein registrierter Transport sendet/empf\u00E4ngt, erscheinen hier ", _jsx("code", { children: "sync-connect" }), ", ", _jsx("code", { children: "sync-append-sent" }), ",", _jsx("code", { children: "sync-events-received" }), " etc."] }));
|
|
126
|
+
}
|
|
127
|
+
return (_jsxs("div", { style: twoColStyle, children: [_jsxs("div", { style: tableScrollStyle, children: [paused && _jsx("div", { style: pausedBannerStyle, children: "\u23F8 Live-Stream pausiert \u2014 Ansicht eingefroren." }), _jsxs("table", { style: tableStyle, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: thStyle, children: "Zeit" }), _jsx("th", { style: thStyle, children: "Dir" }), _jsx("th", { style: thStyle, children: "Kind" }), _jsx("th", { style: thStyle, children: "Transport" }), _jsx("th", { style: thStyle, children: "Stream / Sub" }), _jsx("th", { style: thStyle, children: "Req-ID" }), _jsx("th", { style: thNumStyle, children: "Info" })] }) }), _jsx("tbody", { children: records.slice().reverse().map(r => (_jsx(SyncRow, { captured: r, isSelected: r.seq === selectedSeq, onSelect: () => setSelectedSeq(r.seq === selectedSeq ? null : r.seq) }, r.seq))) })] })] }), _jsx("div", { style: detailColumnStyle, children: selected
|
|
128
|
+
? _jsx(SyncDetail, { captured: selected })
|
|
129
|
+
: _jsx("div", { style: emptyDetailStyle, children: "Klick auf eine Zeile zeigt das vollst\u00E4ndige Wire-Frame." }) })] }));
|
|
130
|
+
}
|
|
131
|
+
function SyncRow({ captured, isSelected, onSelect, }) {
|
|
132
|
+
const r = captured.record;
|
|
133
|
+
const direction = syncDirection(r.kind);
|
|
134
|
+
const dirColor = direction === '↑' ? '#a8d8a8' : direction === '↓' ? '#9ec8ff' : '#888';
|
|
135
|
+
const reqId = 'requestId' in r ? r.requestId : '';
|
|
136
|
+
const subId = 'subscriptionId' in r ? r.subscriptionId : '';
|
|
137
|
+
const streamSubLabel = ('streamId' in r ? r.streamId : '')
|
|
138
|
+
+ (subId ? ` · ${stub(subId)}` : '');
|
|
139
|
+
const info = syncRowInfo(r);
|
|
140
|
+
return (_jsxs("tr", { onClick: onSelect, style: { ...trStyle, ...(isSelected ? trSelectedStyle : null), cursor: 'pointer' }, children: [_jsx("td", { style: tdStyle, children: formatTime(captured.capturedAt) }), _jsx("td", { style: { ...tdStyle, color: dirColor, fontFamily: 'ui-monospace, monospace', fontWeight: 600 }, children: direction }), _jsx("td", { style: { ...tdStyle, fontFamily: 'ui-monospace, monospace' }, children: r.kind.replace('sync-', '') }), _jsx("td", { style: tdStoreStyle, children: captured.transportName }), _jsx("td", { style: tdMonoStyle, children: streamSubLabel || '—' }), _jsx("td", { style: tdMonoStyle, children: reqId ? stub(reqId) : '—' }), _jsx("td", { style: tdNumStyle, children: info })] }));
|
|
141
|
+
}
|
|
142
|
+
function SyncDetail({ captured }) {
|
|
143
|
+
return (_jsxs("div", { style: detailInnerStyle, children: [_jsxs("div", { style: detailHeaderStyle, children: [_jsx("div", { style: { fontWeight: 600, fontSize: 14, fontFamily: 'ui-monospace, monospace' }, children: captured.record.kind }), _jsxs("div", { style: { fontSize: 11, color: '#888', marginTop: 4 }, children: ["#", captured.seq, " \u00B7 Transport ", _jsx("code", { style: inlineCodeStyle, children: captured.transportName }), " ", _jsxs("span", { style: { opacity: 0.6 }, children: ["(id: ", captured.transportId, ")"] })] }), _jsx("div", { style: { fontSize: 11, color: '#888', marginTop: 4 }, children: formatTimeWithMs(captured.capturedAt) })] }), _jsx(Section, { title: "Record", children: _jsx("pre", { style: preStyle, children: safeStringify(captured.record) }) })] }));
|
|
144
|
+
}
|
|
145
|
+
function syncDirection(kind) {
|
|
146
|
+
if (kind === 'sync-append-sent')
|
|
147
|
+
return '↑';
|
|
148
|
+
if (kind === 'sync-append-confirmed' || kind === 'sync-append-rejected' || kind === 'sync-events-received')
|
|
149
|
+
return '↓';
|
|
150
|
+
return '·';
|
|
151
|
+
}
|
|
152
|
+
function syncRowInfo(r) {
|
|
153
|
+
switch (r.kind) {
|
|
154
|
+
case 'sync-connect': return r.url ?? 'connected';
|
|
155
|
+
case 'sync-disconnect': return r.code !== undefined ? `code ${r.code}` : 'disconnect';
|
|
156
|
+
case 'sync-reconnect-scheduled': return `attempt ${r.attempt} in ${r.nextAttemptInMs}ms`;
|
|
157
|
+
case 'sync-append-sent': return `${r.eventCount} ev @ v${r.expectedVersion}`;
|
|
158
|
+
case 'sync-append-confirmed': return `→ v${r.newVersion} (${r.tookMs.toFixed(1)}ms)`;
|
|
159
|
+
case 'sync-append-rejected': return `${r.reason} (exp ${r.expectedVersion}, act ${r.actualVersion})`;
|
|
160
|
+
case 'sync-events-received': return `${r.eventCount} ev → v${r.toVersion}`;
|
|
161
|
+
default: return '';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// ── State-Tab (v0.16.0 Replay-Target) ────────────────────────────
|
|
165
|
+
function StateBody({ targets, registry, }) {
|
|
166
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
167
|
+
const selected = targets.find(t => t.id === selectedId) ?? targets[0] ?? null;
|
|
168
|
+
if (targets.length === 0) {
|
|
169
|
+
return (_jsxs("div", { style: emptyStyle, children: ["Keine ReplayTargets registriert. Variants registrieren ihre Calendar-Stores via ", _jsx("code", { children: "createObservedReplayTarget" }), " \u2014 der Stream-State erscheint hier sobald ein Target l\u00E4uft."] }));
|
|
170
|
+
}
|
|
171
|
+
return (_jsxs("div", { style: twoColStyle, children: [_jsx("div", { style: tableScrollStyle, children: _jsxs("table", { style: tableStyle, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: thStyle, children: "Target" }), _jsx("th", { style: thStyle, children: "Stream" }), _jsx("th", { style: thStyle, children: "Status" }), _jsx("th", { style: thNumStyle, children: "v" }), _jsx("th", { style: thNumStyle, children: "schema" }), _jsx("th", { style: thNumStyle, children: "proj" }), _jsx("th", { style: thNumStyle, children: "blocked" })] }) }), _jsx("tbody", { children: targets.map(t => (_jsx(StateRow, { target: t, isSelected: selected?.id === t.id, onSelect: () => setSelectedId(t.id) }, t.id))) })] }) }), _jsx("div", { style: detailColumnStyle, children: selected
|
|
172
|
+
? _jsx(StateDetail, { target: selected, registry: registry })
|
|
173
|
+
: _jsx("div", { style: emptyDetailStyle, children: "Klick auf eine Zeile zeigt State + ReadModel." }) })] }));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Drift erkannt wenn `schema` und `projection` auseinanderlaufen. Visuell
|
|
177
|
+
* rot markiert in der Tabelle. Beide Werte werden separat angezeigt,
|
|
178
|
+
* sodass der User sofort sieht, in welche Richtung die Drift geht.
|
|
179
|
+
*/
|
|
180
|
+
function isSchemaDrift(schemaV, projV) {
|
|
181
|
+
return schemaV !== undefined && schemaV !== projV;
|
|
182
|
+
}
|
|
183
|
+
function StateRow({ target, isSelected, onSelect, }) {
|
|
184
|
+
const snap = target.snapshot;
|
|
185
|
+
const statusColor = snap.status === 'error' ? '#d97a6b' : snap.status === 'live' ? '#a8d8a8' : '#e0c878';
|
|
186
|
+
const drift = isSchemaDrift(snap.lastAppliedSchemaVersion, target.projectionVersion);
|
|
187
|
+
const driftStyle = drift ? { color: '#d97a6b', fontWeight: 600 } : {};
|
|
188
|
+
return (_jsxs("tr", { onClick: onSelect, style: { ...trStyle, ...(isSelected ? trSelectedStyle : null), cursor: 'pointer' }, children: [_jsx("td", { style: tdStoreStyle, children: target.name }), _jsx("td", { style: tdMonoStyle, children: target.streamId }), _jsx("td", { style: { ...tdStyle, color: statusColor, fontFamily: 'ui-monospace, monospace', fontWeight: 600 }, children: snap.status }), _jsx("td", { style: tdNumStyle, children: snap.lastAppliedVersion }), _jsx("td", { style: { ...tdNumStyle, ...driftStyle }, title: drift ? 'Schema-Drift: Event-schemaVersion ≠ projection.version' : undefined, children: snap.lastAppliedSchemaVersion ?? '—' }), _jsx("td", { style: { ...tdNumStyle, ...driftStyle }, children: target.projectionVersion }), _jsx("td", { style: tdNumStyle, children: snap.blockedEventCount })] }));
|
|
189
|
+
}
|
|
190
|
+
function StateDetail({ target, registry, }) {
|
|
191
|
+
const snap = target.snapshot;
|
|
192
|
+
const drift = isSchemaDrift(snap.lastAppliedSchemaVersion, target.projectionVersion);
|
|
193
|
+
return (_jsxs("div", { style: detailInnerStyle, children: [_jsxs("div", { style: detailHeaderStyle, children: [_jsx("div", { style: { fontWeight: 600, fontSize: 14 }, children: target.name }), _jsxs("div", { style: { fontSize: 11, color: '#888', marginTop: 4 }, children: ["Stream ", _jsx("code", { style: inlineCodeStyle, children: target.streamId }), " \u00B7 ", _jsxs("span", { style: { opacity: 0.6 }, children: ["id: ", target.id] })] }), drift && (_jsxs("div", { style: { marginTop: 8, padding: '6px 10px', background: '#3a2a1a', color: '#ffc88a', fontSize: 11, borderRadius: 4 }, children: ["\u26A0 Schema-Drift: zuletzt applied Event hat ", _jsxs("code", { style: inlineCodeStyle, children: ["schemaVersion=", snap.lastAppliedSchemaVersion] }), ", aber ", _jsxs("code", { style: inlineCodeStyle, children: ["projection.version=", target.projectionVersion] }), ". Upcaster auf Domain-Ebene pr\u00FCfen."] })), _jsxs("div", { style: detailMetaGridStyle, children: [_jsx(MetaPlainRow, { label: "Status", value: snap.status }), _jsx(MetaPlainRow, { label: "Version", value: String(snap.lastAppliedVersion) }), _jsx(MetaPlainRow, { label: "Schema v", value: snap.lastAppliedSchemaVersion !== undefined ? String(snap.lastAppliedSchemaVersion) : '—' }), _jsx(MetaPlainRow, { label: "Projection v", value: String(target.projectionVersion) }), _jsx(MetaPlainRow, { label: "Last event", value: snap.lastAppliedEventId ?? '—', mono: true }), _jsx(MetaPlainRow, { label: "Blocked", value: String(snap.blockedEventCount) })] })] }), snap.lastError && (_jsx(Section, { title: "Last Error", children: _jsxs("pre", { style: preStyle, children: [snap.lastError.error.message, snap.lastError.event ? `\n\nAt event v${snap.lastError.event.envelope.version} (${snap.lastError.event.type})` : '\n\n(from read() — no event)'] }) })), _jsx(TimeTravelSection, { targetId: target.id, liveVersion: snap.lastAppliedVersion, registry: registry }, target.id)] }));
|
|
194
|
+
}
|
|
195
|
+
function TimeTravelSection({ targetId, liveVersion, registry, }) {
|
|
196
|
+
// Pilot-Bug-Fix 2026-06-09: Controller-Lifecycle gehört in `useEffect`,
|
|
197
|
+
// NICHT in `useState`. Vorher: `useState(() => createCtrl)` hielt eine
|
|
198
|
+
// einzige Instanz, die der `useEffect`-Cleanup in StrictMode-Dev sofort
|
|
199
|
+
// nach Mount disposed hat — der Re-Setup-Pass feuerte `goTo` auf der
|
|
200
|
+
// toten Instanz, `view.status='loading'` blieb dauerhaft. In Production
|
|
201
|
+
// (ohne StrictMode) lief das durch, in Dev hing es immer.
|
|
202
|
+
//
|
|
203
|
+
// Lifecycle hier: Setup-1 erzeugt ctrl_A; Cleanup-1 dispose'd + nullt
|
|
204
|
+
// state; Setup-2 erzeugt ctrl_B; Inner re-rendered mit ctrl_B und der
|
|
205
|
+
// useSyncExternalStore-Hook subscribed neu.
|
|
206
|
+
const [controller, setController] = useState(null);
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
const ctrl = registry.createTimeTravelController(targetId);
|
|
209
|
+
setController(ctrl);
|
|
210
|
+
return () => {
|
|
211
|
+
ctrl.dispose();
|
|
212
|
+
setController(curr => (curr === ctrl ? null : curr));
|
|
213
|
+
};
|
|
214
|
+
}, [targetId, registry]);
|
|
215
|
+
if (!controller) {
|
|
216
|
+
return (_jsx(Section, { title: "Time Travel \u00B7 \u2026", children: _jsx("div", { style: { opacity: 0.6, fontSize: 12 }, children: "Initialisiere \u2026" }) }));
|
|
217
|
+
}
|
|
218
|
+
return (_jsx(TimeTravelSectionInner, { controller: controller, liveVersion: liveVersion }));
|
|
219
|
+
}
|
|
220
|
+
function TimeTravelSectionInner({ controller, liveVersion, }) {
|
|
221
|
+
const view = useSyncExternalStore(controller.subscribe, () => controller.view, () => controller.view);
|
|
222
|
+
// Auto-Follow Live: solange `hasUserMoved=false` zieht der Cursor
|
|
223
|
+
// dem Live-Stand nach. Sobald der User scrubt, friert die Position
|
|
224
|
+
// fest. `→ Live`-Button setzt das Flag zurück.
|
|
225
|
+
//
|
|
226
|
+
// Deps bewusst nur `[hasUserMoved, liveVersion, controller]`:
|
|
227
|
+
// - status/cursor SIND NICHT in deps → kein Re-Run pro Status-Tick
|
|
228
|
+
// (kein Spam).
|
|
229
|
+
// - Wenn Live während eines laufenden goTo(oldLive) springt, läuft
|
|
230
|
+
// der Effect mit neuem liveVersion und ruft goTo(newLive). Der
|
|
231
|
+
// Controller invalidiert den alten Read via running-counter
|
|
232
|
+
// (Stale-Cancel). Genau dafür ist der running-counter da.
|
|
233
|
+
const [hasUserMoved, setHasUserMoved] = useState(false);
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (hasUserMoved)
|
|
236
|
+
return;
|
|
237
|
+
void controller.goTo(liveVersion);
|
|
238
|
+
}, [hasUserMoved, liveVersion, controller]);
|
|
239
|
+
const [mode, setMode] = useState('live');
|
|
240
|
+
const [diff, setDiff] = useState([]);
|
|
241
|
+
const [diffLoading, setDiffLoading] = useState(false);
|
|
242
|
+
// Diff: pro view.status sauberen UI-Zustand. Nicht-ready löscht den
|
|
243
|
+
// alten Diff, damit kein stale-Inhalt zur falschen Version stehen
|
|
244
|
+
// bleibt. Promise mit catch/finally + stale-Flag.
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
let stale = false;
|
|
247
|
+
if (view.status !== 'ready') {
|
|
248
|
+
setDiff([]);
|
|
249
|
+
setDiffLoading(view.status === 'loading');
|
|
250
|
+
return () => { stale = true; };
|
|
251
|
+
}
|
|
252
|
+
setDiffLoading(true);
|
|
253
|
+
const compareTo = mode === 'previous' ? Math.max(0, view.cursor - 1) : undefined;
|
|
254
|
+
controller.diff(compareTo).then((d) => {
|
|
255
|
+
if (stale)
|
|
256
|
+
return;
|
|
257
|
+
setDiff(d);
|
|
258
|
+
}, () => {
|
|
259
|
+
if (stale)
|
|
260
|
+
return;
|
|
261
|
+
setDiff([]);
|
|
262
|
+
}).finally(() => {
|
|
263
|
+
if (stale)
|
|
264
|
+
return;
|
|
265
|
+
setDiffLoading(false);
|
|
266
|
+
});
|
|
267
|
+
return () => { stale = true; };
|
|
268
|
+
}, [view.cursor, view.status, mode, controller]);
|
|
269
|
+
const max = Math.max(liveVersion, view.maxVersion);
|
|
270
|
+
// atLive = synchron mit Live (nicht: am Slider-Ende). Falls
|
|
271
|
+
// view.maxVersion temporär > liveVersion, kann der User über
|
|
272
|
+
// liveVersion hinaus scrubben — aber "am Live" heißt explizit
|
|
273
|
+
// cursor === liveVersion.
|
|
274
|
+
const atLive = view.cursor === liveVersion;
|
|
275
|
+
const stateLabel = atLive ? 'Live' : `v${view.cursor}`;
|
|
276
|
+
return (_jsxs(Section, { title: `Time Travel · ${stateLabel} / max v${max}`, children: [_jsxs("div", { style: ttRowStyle, children: [_jsx("input", { type: "range", min: 0, max: max, value: view.cursor, onChange: (e) => {
|
|
277
|
+
setHasUserMoved(true);
|
|
278
|
+
void controller.goTo(Number(e.target.value));
|
|
279
|
+
}, style: sliderStyle, "aria-label": "Time-Travel-Cursor" }), _jsx("button", { type: "button", onClick: () => {
|
|
280
|
+
// → Live = „folge wieder Live": Flag zurücksetzen, der
|
|
281
|
+
// Auto-Follow-Effect macht den goTo(liveVersion). Keine
|
|
282
|
+
// doppelte goTo-Quelle.
|
|
283
|
+
setHasUserMoved(false);
|
|
284
|
+
}, disabled: atLive, style: atLive ? ttBtnDisabledStyle : ttBtnStyle, title: "Auto-Follow Live aktivieren", children: "\u2192 Live" })] }), _jsxs("div", { style: ttMetaRowStyle, children: [_jsxs("span", { children: ["Status: ", _jsx("code", { style: inlineCodeStyle, children: view.status }), view.status === 'loading' && ' · liest Stream …', view.status === 'error' && view.lastError && ` · ${view.lastError.message}`, !hasUserMoved && view.status === 'ready' && ' · auto-follow Live'] }), _jsxs("span", { children: ["Vergleich:", _jsx(DiffModeButton, { mode: mode, setMode: setMode, value: "live", label: "vs Live" }), _jsx(DiffModeButton, { mode: mode, setMode: setMode, value: "previous", label: "vs Previous", disabled: view.cursor === 0 })] })] }), _jsx(Section, { title: `Aggregate State @ ${stateLabel}`, children: _jsx("pre", { style: preStyle, children: safeStringify(view.snapshot.state) }) }), _jsx(Section, { title: `ReadModel @ ${stateLabel}`, children: _jsx("pre", { style: preStyle, children: safeStringify(view.snapshot.readModel) }) }), _jsx(DiffPanel, { diff: diff, loading: diffLoading, mode: mode, cursor: view.cursor, atLive: atLive })] }));
|
|
285
|
+
}
|
|
286
|
+
function DiffModeButton({ mode, setMode, value, label, disabled, }) {
|
|
287
|
+
const active = mode === value;
|
|
288
|
+
return (_jsx("button", { type: "button", onClick: () => setMode(value), disabled: disabled, style: active ? ttToggleActiveStyle : disabled ? ttToggleDisabledStyle : ttToggleStyle, children: label }));
|
|
289
|
+
}
|
|
290
|
+
function DiffPanel({ diff, loading, mode, cursor, atLive, }) {
|
|
291
|
+
if (loading && diff.length === 0) {
|
|
292
|
+
return _jsx("div", { style: diffEmptyStyle, children: "diff l\u00E4dt \u2026" });
|
|
293
|
+
}
|
|
294
|
+
if (mode === 'live' && atLive) {
|
|
295
|
+
return _jsx("div", { style: diffEmptyStyle, children: "Cursor ist am Live-Stand \u2014 kein Diff." });
|
|
296
|
+
}
|
|
297
|
+
if (mode === 'previous' && cursor === 0) {
|
|
298
|
+
return _jsx("div", { style: diffEmptyStyle, children: "v0 hat keinen Vorg\u00E4nger." });
|
|
299
|
+
}
|
|
300
|
+
if (diff.length === 0) {
|
|
301
|
+
return _jsx("div", { style: diffEmptyStyle, children: "Kein Unterschied im Aggregate-State." });
|
|
302
|
+
}
|
|
303
|
+
return (_jsx("div", { style: diffListStyle, children: diff.map((entry, i) => (_jsx(DiffRow, { entry: entry }, `${entry.path}-${i}`))) }));
|
|
304
|
+
}
|
|
305
|
+
function DiffRow({ entry }) {
|
|
306
|
+
const kindColor = entry.kind === 'added' ? '#a8d8a8' :
|
|
307
|
+
entry.kind === 'removed' ? '#d97a6b' :
|
|
308
|
+
'#e0c878';
|
|
309
|
+
return (_jsxs("div", { style: diffRowStyle, children: [_jsx("span", { style: { ...diffKindStyle, color: kindColor }, children: entry.kind === 'added' ? '+' : entry.kind === 'removed' ? '−' : '~' }), _jsx("code", { style: diffPathStyle, children: entry.path || '(root)' }), _jsxs("span", { style: diffArrowStyle, children: [_jsx("code", { style: diffValStyle, children: safeStringify(entry.before) }), _jsx("span", { style: { margin: '0 6px', opacity: 0.5 }, children: "\u2192" }), _jsx("code", { style: diffValStyle, children: safeStringify(entry.after) })] })] }));
|
|
310
|
+
}
|
|
311
|
+
// ── Header ────────────────────────────────────────────────────────
|
|
312
|
+
function PanelHeader({ events, streams, stores, syncTransports, replayTargets, filtered, paused, onTogglePause, onClear, onExport, }) {
|
|
313
|
+
return (_jsxs("div", { style: headerStyle, children: [_jsxs("div", { style: headerRowStyle, children: [_jsx("div", { style: titleStyle, children: "Event-Inspector" }), _jsxs("div", { style: statsStyle, children: [_jsx(Stat, { label: "Events", value: filtered === events ? events : `${filtered} / ${events}` }), _jsx(Stat, { label: "Streams", value: streams }), _jsx(Stat, { label: "Stores", value: stores }), syncTransports > 0 && _jsx(Stat, { label: "Sync", value: syncTransports }), replayTargets > 0 && _jsx(Stat, { label: "Replay", value: replayTargets })] }), _jsx("div", { style: { flex: 1 } }), _jsx("button", { onClick: onTogglePause, style: paused ? buttonActiveStyle : buttonStyle, title: paused ? 'Live-Stream wieder aufnehmen' : 'Live-Stream einfrieren', children: paused ? '▶ Resume' : '⏸ Pause' }), _jsx("button", { onClick: onClear, style: buttonStyle, title: "Sichtbare Events ausblenden (folgende bleiben sichtbar)", children: "Clear" }), _jsx("button", { onClick: onExport, style: buttonStyle, title: "Snapshot des aktuellen DevtoolsState als JSON herunterladen (v0.16.6)", "aria-label": "Snapshot als JSON exportieren", children: "\u21E9 Export" })] }), _jsx("div", { style: subtitleStyle, children: "Live-Stream aller Events aus den registrierten EventStores. Klick auf eine Zeile \u00F6ffnet die Drei-Schichten-Sicht." })] }));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Download-Trigger via temporären `<a download>`. Pilot-Schärfungen:
|
|
317
|
+
* - Date.now() läuft beim Button-Klick im UI-Layer, NICHT im Core-
|
|
318
|
+
* Helper — exportSnapshot bleibt deterministisch.
|
|
319
|
+
* - Cleanup im try/finally — wenn `a.click()` synchron wirft (manche
|
|
320
|
+
* Browser-Policies, Test-Mocks), MÜSSEN trotzdem removeChild und
|
|
321
|
+
* revokeObjectURL laufen. Sonst URL-Leak + DOM-Müll.
|
|
322
|
+
*/
|
|
323
|
+
function triggerDownload(filename, content) {
|
|
324
|
+
const blob = new Blob([content], { type: 'application/json' });
|
|
325
|
+
const url = URL.createObjectURL(blob);
|
|
326
|
+
const a = document.createElement('a');
|
|
327
|
+
a.href = url;
|
|
328
|
+
a.download = filename;
|
|
329
|
+
document.body.appendChild(a);
|
|
330
|
+
try {
|
|
331
|
+
a.click();
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
document.body.removeChild(a);
|
|
335
|
+
URL.revokeObjectURL(url);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function Stat({ label, value }) {
|
|
339
|
+
return (_jsxs("span", { style: statItemStyle, children: [_jsx("span", { style: statValueStyle, children: value }), _jsx("span", { style: statLabelStyle, children: label })] }));
|
|
340
|
+
}
|
|
341
|
+
// ── Filter-Bar ────────────────────────────────────────────────────
|
|
342
|
+
function FilterBar({ stores, storeFilter, onStoreFilter, typeFilter, onTypeFilter, streamFilter, onStreamFilter, }) {
|
|
343
|
+
return (_jsxs("div", { style: filterBarStyle, children: [_jsxs("select", { value: storeFilter, onChange: e => onStoreFilter(e.target.value), style: selectStyle, title: "Auf einen Store filtern", children: [_jsx("option", { value: "", children: "Alle Stores" }), stores.map(s => (_jsx("option", { value: s.id, children: s.name }, s.id)))] }), _jsx("input", { type: "text", placeholder: "Type filter (substring)", value: typeFilter, onChange: e => onTypeFilter(e.target.value), style: inputStyle }), _jsx("input", { type: "text", placeholder: "Stream filter (substring)", value: streamFilter, onChange: e => onStreamFilter(e.target.value), style: inputStyle })] }));
|
|
344
|
+
}
|
|
345
|
+
// ── Events-Body (Tabelle + Detail-Sidebar) ────────────────────────
|
|
346
|
+
function EventsBody({ state, visible, selected, selectedSeq, onSelect, paused, }) {
|
|
347
|
+
if (state.eventStores.length === 0 && state.events.length === 0) {
|
|
348
|
+
return (_jsx("div", { style: emptyStyle, children: "Keine EventStores registriert und kein History-Buffer. Wechsel zur Variant \u201EMinimal\", \u201EMulti-View\", \u2026 \u2014 sobald eine Variant einen observed Store mountet, erscheinen die Events hier." }));
|
|
349
|
+
}
|
|
350
|
+
if (state.events.length === 0) {
|
|
351
|
+
return (_jsxs("div", { style: emptyStyle, children: ["EventStore registriert (", state.eventStores.map(s => s.name).join(', '), "), aber noch keine Events. Wechsel zu einer Calendar-Variant und lege einen Termin an."] }));
|
|
352
|
+
}
|
|
353
|
+
// state.events.length > 0 — auch wenn eventStores leer ist (Variant
|
|
354
|
+
// wurde gewechselt, rolling-window-Buffer überlebt). Tabelle rendern.
|
|
355
|
+
if (visible.length === 0) {
|
|
356
|
+
return (_jsx("div", { style: emptyStyle, children: "Keine Events sichtbar. Filter ggf. zur\u00FCcksetzen oder \u201EClear\" r\u00FCckg\u00E4ngig (Variant neu \u00F6ffnen)." }));
|
|
357
|
+
}
|
|
358
|
+
return (_jsxs("div", { style: twoColStyle, children: [_jsxs("div", { style: tableScrollStyle, children: [paused && _jsx("div", { style: pausedBannerStyle, children: "\u23F8 Live-Stream pausiert \u2014 Ansicht eingefroren." }), _jsxs("table", { style: tableStyle, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: thStyle, children: "Zeit" }), _jsx("th", { style: thStyle, children: "Store" }), _jsx("th", { style: thStyle, children: "Type" }), _jsx("th", { style: thStyle, children: "Label" }), _jsx("th", { style: thStyle, children: "Stream" }), _jsx("th", { style: thStyle, children: "Entity" }), _jsx("th", { style: thNumStyle, children: "v" }), _jsx("th", { style: thNumStyle, children: "sv" }), _jsx("th", { style: thStyle, children: "Event-ID" }), _jsx("th", { style: thStyle, children: "Corr" }), _jsx("th", { style: thStyle, children: "Cau" }), _jsx("th", { style: thNumStyle, children: "tookMs" })] }) }), _jsx("tbody", { children: visible.slice().reverse().map(e => (_jsx(EventRow, { captured: e, isSelected: e.seq === selectedSeq, onSelect: () => onSelect(e.seq === selectedSeq ? null : e.seq) }, e.seq))) })] })] }), _jsx("div", { style: detailColumnStyle, children: selected
|
|
359
|
+
? _jsx(EventDetail, { captured: selected })
|
|
360
|
+
: _jsx("div", { style: emptyDetailStyle, children: "Klick auf eine Zeile zeigt Envelope, Metadata und Payload." }) })] }));
|
|
361
|
+
}
|
|
362
|
+
function EventRow({ captured, isSelected, onSelect, }) {
|
|
363
|
+
const { event, streamId, storeName, tookMs } = captured;
|
|
364
|
+
const label = typeToLabel(event.type);
|
|
365
|
+
const sev = typeSeverity(event.type);
|
|
366
|
+
const schemaVersion = event.metadata.schemaVersion;
|
|
367
|
+
return (_jsxs("tr", { onClick: onSelect, style: { ...trStyle, ...(isSelected ? trSelectedStyle : null), cursor: 'pointer' }, children: [_jsx("td", { style: tdStyle, children: formatTime(event.envelope.timestamp) }), _jsx("td", { style: tdStoreStyle, children: storeName }), _jsx("td", { style: { ...tdStyle, color: sev === 'destructive' ? '#d97a6b' : '#5da3dc', fontFamily: 'ui-monospace, monospace' }, children: event.type }), _jsx("td", { style: tdStyle, children: label }), _jsx("td", { style: tdMonoStyle, children: streamId }), _jsx("td", { style: tdMonoStyle, children: event.envelope.entityId }), _jsx("td", { style: tdNumStyle, children: event.envelope.version }), _jsx("td", { style: tdNumStyle, children: schemaVersion ?? '—' }), _jsx("td", { style: tdMonoStyle, children: stub(event.envelope.id) }), _jsx("td", { style: tdMonoStyle, children: stub(event.metadata.correlationId) }), _jsx("td", { style: tdMonoStyle, children: stub(event.metadata.causationId) }), _jsx("td", { style: tdNumStyle, children: tookMs.toFixed(2) })] }));
|
|
368
|
+
}
|
|
369
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
370
|
+
function formatTime(ms) {
|
|
371
|
+
const d = new Date(ms);
|
|
372
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
373
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
374
|
+
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
375
|
+
return `${hh}:${mm}:${ss}`;
|
|
376
|
+
}
|
|
377
|
+
function typeToLabel(type) {
|
|
378
|
+
const lastSegment = type.split('.').pop() ?? type;
|
|
379
|
+
const spaced = lastSegment.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
380
|
+
if (spaced.length === 0)
|
|
381
|
+
return type;
|
|
382
|
+
return spaced[0].toUpperCase() + spaced.slice(1).toLowerCase();
|
|
383
|
+
}
|
|
384
|
+
function typeSeverity(type) {
|
|
385
|
+
return /Cancelled|Deleted|Failed|Rejected|Declined/i.test(type) ? 'destructive' : 'normal';
|
|
386
|
+
}
|
|
387
|
+
// ── Styles ─────────────────────────────────────────────────────────
|
|
388
|
+
const pageStyle = {
|
|
389
|
+
display: 'flex',
|
|
390
|
+
flexDirection: 'column',
|
|
391
|
+
height: '100%',
|
|
392
|
+
background: '#1a1a1a',
|
|
393
|
+
color: '#eee',
|
|
394
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
395
|
+
fontSize: 13,
|
|
396
|
+
};
|
|
397
|
+
const headerStyle = {
|
|
398
|
+
padding: '10px 16px 8px',
|
|
399
|
+
background: '#222',
|
|
400
|
+
borderBottom: '1px solid #333',
|
|
401
|
+
};
|
|
402
|
+
const headerRowStyle = {
|
|
403
|
+
display: 'flex',
|
|
404
|
+
alignItems: 'center',
|
|
405
|
+
gap: 12,
|
|
406
|
+
};
|
|
407
|
+
const titleStyle = {
|
|
408
|
+
fontSize: 14,
|
|
409
|
+
fontWeight: 600,
|
|
410
|
+
color: '#fff',
|
|
411
|
+
};
|
|
412
|
+
const subtitleStyle = {
|
|
413
|
+
marginTop: 6,
|
|
414
|
+
fontSize: 11,
|
|
415
|
+
color: '#888',
|
|
416
|
+
};
|
|
417
|
+
const statsStyle = {
|
|
418
|
+
display: 'flex',
|
|
419
|
+
gap: 14,
|
|
420
|
+
marginLeft: 8,
|
|
421
|
+
};
|
|
422
|
+
const statItemStyle = {
|
|
423
|
+
display: 'inline-flex',
|
|
424
|
+
alignItems: 'baseline',
|
|
425
|
+
gap: 4,
|
|
426
|
+
};
|
|
427
|
+
const statValueStyle = {
|
|
428
|
+
fontFamily: 'ui-monospace, "SF Mono", monospace',
|
|
429
|
+
fontSize: 13,
|
|
430
|
+
fontWeight: 600,
|
|
431
|
+
color: '#fff',
|
|
432
|
+
};
|
|
433
|
+
const statLabelStyle = {
|
|
434
|
+
fontSize: 10,
|
|
435
|
+
textTransform: 'uppercase',
|
|
436
|
+
letterSpacing: 0.5,
|
|
437
|
+
color: '#888',
|
|
438
|
+
};
|
|
439
|
+
const tabBarStyle = {
|
|
440
|
+
display: 'flex',
|
|
441
|
+
gap: 0,
|
|
442
|
+
background: '#1e1e1e',
|
|
443
|
+
borderBottom: '1px solid #2a2a2a',
|
|
444
|
+
padding: '0 16px',
|
|
445
|
+
};
|
|
446
|
+
const tabStyle = {
|
|
447
|
+
background: 'transparent',
|
|
448
|
+
border: 'none',
|
|
449
|
+
color: '#888',
|
|
450
|
+
padding: '10px 16px',
|
|
451
|
+
fontSize: 12,
|
|
452
|
+
cursor: 'pointer',
|
|
453
|
+
borderBottom: '2px solid transparent',
|
|
454
|
+
};
|
|
455
|
+
const tabActiveStyle = {
|
|
456
|
+
...tabStyle,
|
|
457
|
+
color: '#fff',
|
|
458
|
+
borderBottom: '2px solid #5082dc',
|
|
459
|
+
fontWeight: 600,
|
|
460
|
+
};
|
|
461
|
+
const filterBarStyle = {
|
|
462
|
+
display: 'flex',
|
|
463
|
+
gap: 8,
|
|
464
|
+
padding: '8px 16px',
|
|
465
|
+
background: '#1e1e1e',
|
|
466
|
+
borderBottom: '1px solid #2a2a2a',
|
|
467
|
+
};
|
|
468
|
+
const selectStyle = {
|
|
469
|
+
background: '#262626',
|
|
470
|
+
color: '#eee',
|
|
471
|
+
border: '1px solid #3a3a3a',
|
|
472
|
+
borderRadius: 4,
|
|
473
|
+
padding: '4px 8px',
|
|
474
|
+
fontSize: 12,
|
|
475
|
+
minWidth: 140,
|
|
476
|
+
};
|
|
477
|
+
const inputStyle = {
|
|
478
|
+
background: '#262626',
|
|
479
|
+
color: '#eee',
|
|
480
|
+
border: '1px solid #3a3a3a',
|
|
481
|
+
borderRadius: 4,
|
|
482
|
+
padding: '4px 8px',
|
|
483
|
+
fontSize: 12,
|
|
484
|
+
flex: 1,
|
|
485
|
+
minWidth: 0,
|
|
486
|
+
};
|
|
487
|
+
const buttonStyle = {
|
|
488
|
+
background: '#2e2e2e',
|
|
489
|
+
color: '#ddd',
|
|
490
|
+
border: '1px solid #3a3a3a',
|
|
491
|
+
borderRadius: 4,
|
|
492
|
+
padding: '4px 10px',
|
|
493
|
+
fontSize: 12,
|
|
494
|
+
cursor: 'pointer',
|
|
495
|
+
};
|
|
496
|
+
const buttonActiveStyle = {
|
|
497
|
+
...buttonStyle,
|
|
498
|
+
background: '#3a4a6a',
|
|
499
|
+
borderColor: '#5082dc',
|
|
500
|
+
color: '#fff',
|
|
501
|
+
};
|
|
502
|
+
const pausedBannerStyle = {
|
|
503
|
+
padding: '6px 12px',
|
|
504
|
+
background: '#3a2a1a',
|
|
505
|
+
color: '#ffc88a',
|
|
506
|
+
borderBottom: '1px solid #443322',
|
|
507
|
+
fontSize: 12,
|
|
508
|
+
};
|
|
509
|
+
const tableStyle = {
|
|
510
|
+
width: '100%',
|
|
511
|
+
borderCollapse: 'collapse',
|
|
512
|
+
fontSize: 12,
|
|
513
|
+
};
|
|
514
|
+
const thStyle = {
|
|
515
|
+
textAlign: 'left',
|
|
516
|
+
padding: '8px 10px',
|
|
517
|
+
background: '#1e1e1e',
|
|
518
|
+
borderBottom: '1px solid #333',
|
|
519
|
+
position: 'sticky',
|
|
520
|
+
top: 0,
|
|
521
|
+
fontWeight: 600,
|
|
522
|
+
fontSize: 11,
|
|
523
|
+
textTransform: 'uppercase',
|
|
524
|
+
letterSpacing: 0.4,
|
|
525
|
+
color: '#999',
|
|
526
|
+
};
|
|
527
|
+
const thNumStyle = {
|
|
528
|
+
...thStyle,
|
|
529
|
+
textAlign: 'right',
|
|
530
|
+
};
|
|
531
|
+
const trStyle = {
|
|
532
|
+
borderBottom: '1px solid #2a2a2a',
|
|
533
|
+
};
|
|
534
|
+
const trSelectedStyle = {
|
|
535
|
+
background: 'rgba(80, 130, 220, 0.22)',
|
|
536
|
+
outline: '1px solid rgba(80, 130, 220, 0.55)',
|
|
537
|
+
outlineOffset: -1,
|
|
538
|
+
};
|
|
539
|
+
const tdStyle = {
|
|
540
|
+
padding: '6px 10px',
|
|
541
|
+
fontSize: 12,
|
|
542
|
+
};
|
|
543
|
+
const tdMonoStyle = {
|
|
544
|
+
...tdStyle,
|
|
545
|
+
fontFamily: 'ui-monospace, "SF Mono", monospace',
|
|
546
|
+
fontSize: 11,
|
|
547
|
+
color: '#bbb',
|
|
548
|
+
};
|
|
549
|
+
const tdNumStyle = {
|
|
550
|
+
...tdStyle,
|
|
551
|
+
fontFamily: 'ui-monospace, "SF Mono", monospace',
|
|
552
|
+
fontSize: 11,
|
|
553
|
+
color: '#888',
|
|
554
|
+
textAlign: 'right',
|
|
555
|
+
};
|
|
556
|
+
const tdStoreStyle = {
|
|
557
|
+
...tdStyle,
|
|
558
|
+
fontSize: 11,
|
|
559
|
+
color: '#9ec8ff',
|
|
560
|
+
};
|
|
561
|
+
// ── Time-Travel ────────────────────────────────────────────────────
|
|
562
|
+
const ttRowStyle = {
|
|
563
|
+
display: 'flex',
|
|
564
|
+
alignItems: 'center',
|
|
565
|
+
gap: 10,
|
|
566
|
+
padding: '8px 0 4px',
|
|
567
|
+
};
|
|
568
|
+
const sliderStyle = {
|
|
569
|
+
flex: 1,
|
|
570
|
+
accentColor: '#7aaef5',
|
|
571
|
+
};
|
|
572
|
+
const ttBtnStyle = {
|
|
573
|
+
background: '#2a2a2a',
|
|
574
|
+
color: '#ddd',
|
|
575
|
+
border: '1px solid #444',
|
|
576
|
+
borderRadius: 4,
|
|
577
|
+
padding: '4px 10px',
|
|
578
|
+
fontSize: 11,
|
|
579
|
+
cursor: 'pointer',
|
|
580
|
+
};
|
|
581
|
+
const ttBtnDisabledStyle = {
|
|
582
|
+
...ttBtnStyle,
|
|
583
|
+
opacity: 0.4,
|
|
584
|
+
cursor: 'default',
|
|
585
|
+
};
|
|
586
|
+
const ttMetaRowStyle = {
|
|
587
|
+
display: 'flex',
|
|
588
|
+
justifyContent: 'space-between',
|
|
589
|
+
alignItems: 'center',
|
|
590
|
+
fontSize: 11,
|
|
591
|
+
color: '#999',
|
|
592
|
+
padding: '4px 0 8px',
|
|
593
|
+
gap: 10,
|
|
594
|
+
};
|
|
595
|
+
const ttToggleStyle = {
|
|
596
|
+
background: 'transparent',
|
|
597
|
+
color: '#bbb',
|
|
598
|
+
border: '1px solid #333',
|
|
599
|
+
borderRadius: 3,
|
|
600
|
+
padding: '2px 8px',
|
|
601
|
+
marginLeft: 6,
|
|
602
|
+
fontSize: 11,
|
|
603
|
+
cursor: 'pointer',
|
|
604
|
+
};
|
|
605
|
+
const ttToggleActiveStyle = {
|
|
606
|
+
...ttToggleStyle,
|
|
607
|
+
background: '#3a4f6a',
|
|
608
|
+
borderColor: '#5a7aa6',
|
|
609
|
+
color: '#e8f0ff',
|
|
610
|
+
};
|
|
611
|
+
const ttToggleDisabledStyle = {
|
|
612
|
+
...ttToggleStyle,
|
|
613
|
+
opacity: 0.4,
|
|
614
|
+
cursor: 'default',
|
|
615
|
+
};
|
|
616
|
+
const diffEmptyStyle = {
|
|
617
|
+
padding: '8px 12px',
|
|
618
|
+
fontSize: 11,
|
|
619
|
+
color: '#888',
|
|
620
|
+
background: '#161616',
|
|
621
|
+
borderRadius: 4,
|
|
622
|
+
fontStyle: 'italic',
|
|
623
|
+
};
|
|
624
|
+
const diffListStyle = {
|
|
625
|
+
display: 'flex',
|
|
626
|
+
flexDirection: 'column',
|
|
627
|
+
gap: 2,
|
|
628
|
+
fontSize: 11,
|
|
629
|
+
background: '#161616',
|
|
630
|
+
borderRadius: 4,
|
|
631
|
+
padding: 6,
|
|
632
|
+
maxHeight: 240,
|
|
633
|
+
overflow: 'auto',
|
|
634
|
+
};
|
|
635
|
+
const diffRowStyle = {
|
|
636
|
+
display: 'grid',
|
|
637
|
+
gridTemplateColumns: '16px minmax(80px, 200px) 1fr',
|
|
638
|
+
alignItems: 'center',
|
|
639
|
+
gap: 6,
|
|
640
|
+
padding: '2px 4px',
|
|
641
|
+
fontFamily: 'ui-monospace, "SF Mono", monospace',
|
|
642
|
+
};
|
|
643
|
+
const diffKindStyle = {
|
|
644
|
+
fontWeight: 700,
|
|
645
|
+
textAlign: 'center',
|
|
646
|
+
};
|
|
647
|
+
const diffPathStyle = {
|
|
648
|
+
fontSize: 11,
|
|
649
|
+
color: '#ddd',
|
|
650
|
+
overflow: 'hidden',
|
|
651
|
+
textOverflow: 'ellipsis',
|
|
652
|
+
whiteSpace: 'nowrap',
|
|
653
|
+
};
|
|
654
|
+
const diffArrowStyle = {
|
|
655
|
+
display: 'flex',
|
|
656
|
+
alignItems: 'center',
|
|
657
|
+
overflow: 'hidden',
|
|
658
|
+
};
|
|
659
|
+
const diffValStyle = {
|
|
660
|
+
fontSize: 11,
|
|
661
|
+
background: '#0f0f0f',
|
|
662
|
+
padding: '1px 5px',
|
|
663
|
+
borderRadius: 3,
|
|
664
|
+
whiteSpace: 'nowrap',
|
|
665
|
+
overflow: 'hidden',
|
|
666
|
+
textOverflow: 'ellipsis',
|
|
667
|
+
maxWidth: '40%',
|
|
668
|
+
};
|
|
669
|
+
//# sourceMappingURL=EventInspectorPanel.js.map
|