@principal-ai/principal-view-react 0.14.21 → 0.14.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +23 -10
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/state-view/PipelineView.d.ts +13 -0
- package/dist/components/state-view/PipelineView.d.ts.map +1 -0
- package/dist/components/state-view/PipelineView.js +195 -0
- package/dist/components/state-view/PipelineView.js.map +1 -0
- package/dist/components/state-view/index.d.ts +14 -0
- package/dist/components/state-view/index.d.ts.map +1 -0
- package/dist/components/state-view/index.js +12 -0
- package/dist/components/state-view/index.js.map +1 -0
- package/dist/components/state-view/types.d.ts +188 -0
- package/dist/components/state-view/types.d.ts.map +1 -0
- package/dist/components/state-view/types.js +10 -0
- package/dist/components/state-view/types.js.map +1 -0
- package/dist/components/state-view/useStateView.d.ts +32 -0
- package/dist/components/state-view/useStateView.d.ts.map +1 -0
- package/dist/components/state-view/useStateView.js +129 -0
- package/dist/components/state-view/useStateView.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/nodes/CustomNode.js +8 -8
- package/dist/nodes/CustomNode.js.map +1 -1
- package/dist/nodes/otel/OtelBoundaryNode.d.ts.map +1 -1
- package/dist/nodes/otel/OtelBoundaryNode.js +5 -3
- package/dist/nodes/otel/OtelBoundaryNode.js.map +1 -1
- package/dist/nodes/otel/OtelEventNode.d.ts.map +1 -1
- package/dist/nodes/otel/OtelEventNode.js +5 -3
- package/dist/nodes/otel/OtelEventNode.js.map +1 -1
- package/dist/nodes/otel/OtelResourceNode.d.ts.map +1 -1
- package/dist/nodes/otel/OtelResourceNode.js +5 -3
- package/dist/nodes/otel/OtelResourceNode.js.map +1 -1
- package/dist/nodes/otel/OtelScopeNode.d.ts.map +1 -1
- package/dist/nodes/otel/OtelScopeNode.js +5 -3
- package/dist/nodes/otel/OtelScopeNode.js.map +1 -1
- package/dist/nodes/otel/OtelSpanConventionNode.d.ts.map +1 -1
- package/dist/nodes/otel/OtelSpanConventionNode.js +5 -3
- package/dist/nodes/otel/OtelSpanConventionNode.js.map +1 -1
- package/package.json +2 -2
- package/src/components/GraphRenderer.tsx +24 -10
- package/src/components/state-view/PipelineView.tsx +347 -0
- package/src/components/state-view/index.ts +14 -0
- package/src/components/state-view/types.ts +261 -0
- package/src/components/state-view/useStateView.ts +205 -0
- package/src/index.ts +36 -0
- package/src/nodes/CustomNode.tsx +8 -8
- package/src/nodes/otel/OtelBoundaryNode.tsx +5 -3
- package/src/nodes/otel/OtelEventNode.tsx +5 -3
- package/src/nodes/otel/OtelResourceNode.tsx +5 -3
- package/src/nodes/otel/OtelScopeNode.tsx +5 -3
- package/src/nodes/otel/OtelSpanConventionNode.tsx +5 -4
- package/src/stories/CanvasEdgeTypes.stories.tsx +23 -27
- package/src/stories/GraphRenderer.stories.tsx +144 -200
- package/src/stories/StateView.stories.tsx +417 -0
- package/src/stories/__traces__/test-run.canvas.json +27 -30
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PipelineView Component
|
|
3
|
+
*
|
|
4
|
+
* A state-driven visualization showing data flowing through pipeline stages.
|
|
5
|
+
* Demonstrates the state view pattern with animations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import type {
|
|
10
|
+
PipelineState,
|
|
11
|
+
PipelineEvent,
|
|
12
|
+
PipelineStage,
|
|
13
|
+
EventSource,
|
|
14
|
+
TransitionDefinition,
|
|
15
|
+
} from './types';
|
|
16
|
+
import { useStateView } from './useStateView';
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Reducer
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
const initialPipelineState: PipelineState = {
|
|
23
|
+
stages: {
|
|
24
|
+
disk: { id: 'disk', label: 'Disk', lastEventTime: null, isActive: false },
|
|
25
|
+
watch: { id: 'watch', label: 'Watch', lastEventTime: null, isActive: false },
|
|
26
|
+
cache: { id: 'cache', label: 'Cache', lastEventTime: null, isActive: false, count: 0 },
|
|
27
|
+
event: { id: 'event', label: 'Events', lastEventTime: null, isActive: false, count: 0 },
|
|
28
|
+
},
|
|
29
|
+
repos: new Map(),
|
|
30
|
+
totalEvents: 0,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function pipelineReducer(state: PipelineState, event: PipelineEvent): PipelineState {
|
|
34
|
+
const now = event.timestamp;
|
|
35
|
+
|
|
36
|
+
switch (event.type) {
|
|
37
|
+
case 'FS_CHANGE':
|
|
38
|
+
return {
|
|
39
|
+
...state,
|
|
40
|
+
stages: {
|
|
41
|
+
...state.stages,
|
|
42
|
+
disk: { ...state.stages.disk, lastEventTime: now, isActive: true },
|
|
43
|
+
},
|
|
44
|
+
totalEvents: state.totalEvents + 1,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
case 'WATCH_DETECTED':
|
|
48
|
+
return {
|
|
49
|
+
...state,
|
|
50
|
+
stages: {
|
|
51
|
+
...state.stages,
|
|
52
|
+
watch: { ...state.stages.watch, lastEventTime: now, isActive: true },
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
case 'CACHE_REBUILD':
|
|
57
|
+
case 'CACHE_HIT': {
|
|
58
|
+
const newRepos = new Map(state.repos);
|
|
59
|
+
if (event.payload.repo) {
|
|
60
|
+
newRepos.set(event.payload.repo, {
|
|
61
|
+
path: event.payload.repo,
|
|
62
|
+
lastEventTime: now,
|
|
63
|
+
lastEventType: event.type,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...state,
|
|
68
|
+
stages: {
|
|
69
|
+
...state.stages,
|
|
70
|
+
cache: {
|
|
71
|
+
...state.stages.cache,
|
|
72
|
+
lastEventTime: now,
|
|
73
|
+
isActive: true,
|
|
74
|
+
count: (state.stages.cache.count ?? 0) + 1,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
repos: newRepos,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'EVENT_BROADCAST':
|
|
82
|
+
return {
|
|
83
|
+
...state,
|
|
84
|
+
stages: {
|
|
85
|
+
...state.stages,
|
|
86
|
+
event: {
|
|
87
|
+
...state.stages.event,
|
|
88
|
+
lastEventTime: now,
|
|
89
|
+
isActive: true,
|
|
90
|
+
count: (state.stages.event.count ?? 0) + 1,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
default:
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// Transitions
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
const pipelineTransitions: TransitionDefinition[] = [
|
|
105
|
+
{ watch: 'stages.disk.lastEventTime', condition: 'changed', animate: 'pulse', target: 'stage-disk' },
|
|
106
|
+
{ watch: 'stages.watch.lastEventTime', condition: 'changed', animate: 'pulse', target: 'stage-watch' },
|
|
107
|
+
{ watch: 'stages.cache.lastEventTime', condition: 'changed', animate: 'pulse', target: 'stage-cache' },
|
|
108
|
+
{ watch: 'stages.event.lastEventTime', condition: 'changed', animate: 'pulse', target: 'stage-event' },
|
|
109
|
+
{ watch: 'stages.event.count', condition: 'increased', animate: 'particle-flow', target: 'flow-cache-event' },
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
// =============================================================================
|
|
113
|
+
// Sub-Components
|
|
114
|
+
// =============================================================================
|
|
115
|
+
|
|
116
|
+
interface StageBoxProps {
|
|
117
|
+
stage: PipelineStage;
|
|
118
|
+
isAnimating: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function StageBox({ stage, isAnimating }: StageBoxProps) {
|
|
122
|
+
const baseStyle: React.CSSProperties = {
|
|
123
|
+
display: 'flex',
|
|
124
|
+
flexDirection: 'column',
|
|
125
|
+
alignItems: 'center',
|
|
126
|
+
justifyContent: 'center',
|
|
127
|
+
width: 100,
|
|
128
|
+
height: 80,
|
|
129
|
+
borderRadius: 8,
|
|
130
|
+
backgroundColor: stage.isActive ? '#3b82f6' : '#374151',
|
|
131
|
+
color: 'white',
|
|
132
|
+
fontFamily: 'system-ui, sans-serif',
|
|
133
|
+
transition: 'all 0.3s ease',
|
|
134
|
+
boxShadow: isAnimating ? '0 0 20px rgba(59, 130, 246, 0.8)' : 'none',
|
|
135
|
+
transform: isAnimating ? 'scale(1.05)' : 'scale(1)',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div id={`stage-${stage.id}`} style={baseStyle}>
|
|
140
|
+
<div style={{ fontWeight: 'bold', fontSize: 14 }}>{stage.label}</div>
|
|
141
|
+
{stage.count !== undefined && (
|
|
142
|
+
<div style={{ fontSize: 20, fontWeight: 'bold' }}>{stage.count}</div>
|
|
143
|
+
)}
|
|
144
|
+
{stage.lastEventTime && (
|
|
145
|
+
<div style={{ fontSize: 10, opacity: 0.7 }}>
|
|
146
|
+
{formatTimeAgo(stage.lastEventTime)}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface FlowArrowProps {
|
|
154
|
+
id: string;
|
|
155
|
+
isAnimating: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function FlowArrow({ id, isAnimating }: FlowArrowProps) {
|
|
159
|
+
const style: React.CSSProperties = {
|
|
160
|
+
display: 'flex',
|
|
161
|
+
alignItems: 'center',
|
|
162
|
+
justifyContent: 'center',
|
|
163
|
+
width: 60,
|
|
164
|
+
height: 40,
|
|
165
|
+
color: isAnimating ? '#3b82f6' : '#6b7280',
|
|
166
|
+
fontSize: 24,
|
|
167
|
+
transition: 'color 0.3s ease',
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div id={id} style={style}>
|
|
172
|
+
{isAnimating ? (
|
|
173
|
+
<span style={{ animation: 'pulse 0.5s ease-in-out' }}>→→→</span>
|
|
174
|
+
) : (
|
|
175
|
+
'→'
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function formatTimeAgo(timestamp: number): string {
|
|
182
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
183
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
184
|
+
const minutes = Math.floor(seconds / 60);
|
|
185
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
186
|
+
const hours = Math.floor(minutes / 60);
|
|
187
|
+
return `${hours}h ago`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// =============================================================================
|
|
191
|
+
// Main Component
|
|
192
|
+
// =============================================================================
|
|
193
|
+
|
|
194
|
+
export interface PipelineViewProps {
|
|
195
|
+
eventSource: EventSource<PipelineEvent>;
|
|
196
|
+
title?: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function PipelineView({ eventSource, title = 'Pipeline View' }: PipelineViewProps) {
|
|
200
|
+
const { state, animations, isReplay, reset } = useStateView<PipelineState, PipelineEvent>({
|
|
201
|
+
initialState: initialPipelineState,
|
|
202
|
+
reducer: pipelineReducer,
|
|
203
|
+
eventSource,
|
|
204
|
+
transitions: pipelineTransitions,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const isStageAnimating = (stageId: string) =>
|
|
208
|
+
animations.some((a) => a.target === `stage-${stageId}`);
|
|
209
|
+
|
|
210
|
+
const isFlowAnimating = (flowId: string) =>
|
|
211
|
+
animations.some((a) => a.target === flowId);
|
|
212
|
+
|
|
213
|
+
const containerStyle: React.CSSProperties = {
|
|
214
|
+
fontFamily: 'system-ui, sans-serif',
|
|
215
|
+
backgroundColor: '#1f2937',
|
|
216
|
+
color: 'white',
|
|
217
|
+
padding: 24,
|
|
218
|
+
borderRadius: 12,
|
|
219
|
+
minWidth: 600,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const headerStyle: React.CSSProperties = {
|
|
223
|
+
display: 'flex',
|
|
224
|
+
justifyContent: 'space-between',
|
|
225
|
+
alignItems: 'center',
|
|
226
|
+
marginBottom: 24,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const pipelineStyle: React.CSSProperties = {
|
|
230
|
+
display: 'flex',
|
|
231
|
+
alignItems: 'center',
|
|
232
|
+
justifyContent: 'center',
|
|
233
|
+
gap: 8,
|
|
234
|
+
marginBottom: 24,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const statsStyle: React.CSSProperties = {
|
|
238
|
+
display: 'flex',
|
|
239
|
+
gap: 24,
|
|
240
|
+
justifyContent: 'center',
|
|
241
|
+
padding: '16px 0',
|
|
242
|
+
borderTop: '1px solid #374151',
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const statBoxStyle: React.CSSProperties = {
|
|
246
|
+
textAlign: 'center',
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const repoListStyle: React.CSSProperties = {
|
|
250
|
+
marginTop: 16,
|
|
251
|
+
padding: 16,
|
|
252
|
+
backgroundColor: '#111827',
|
|
253
|
+
borderRadius: 8,
|
|
254
|
+
maxHeight: 200,
|
|
255
|
+
overflowY: 'auto',
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div style={containerStyle}>
|
|
260
|
+
<div style={headerStyle}>
|
|
261
|
+
<h2 style={{ margin: 0 }}>{title}</h2>
|
|
262
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
263
|
+
{isReplay && (
|
|
264
|
+
<span style={{ fontSize: 12, color: '#f59e0b' }}>REPLAY</span>
|
|
265
|
+
)}
|
|
266
|
+
<button
|
|
267
|
+
onClick={reset}
|
|
268
|
+
style={{
|
|
269
|
+
padding: '4px 12px',
|
|
270
|
+
borderRadius: 4,
|
|
271
|
+
border: 'none',
|
|
272
|
+
backgroundColor: '#374151',
|
|
273
|
+
color: 'white',
|
|
274
|
+
cursor: 'pointer',
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
Reset
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{/* Pipeline Stages */}
|
|
283
|
+
<div style={pipelineStyle}>
|
|
284
|
+
<StageBox stage={state.stages.disk} isAnimating={isStageAnimating('disk')} />
|
|
285
|
+
<FlowArrow id="flow-disk-watch" isAnimating={isFlowAnimating('flow-disk-watch')} />
|
|
286
|
+
<StageBox stage={state.stages.watch} isAnimating={isStageAnimating('watch')} />
|
|
287
|
+
<FlowArrow id="flow-watch-cache" isAnimating={isFlowAnimating('flow-watch-cache')} />
|
|
288
|
+
<StageBox stage={state.stages.cache} isAnimating={isStageAnimating('cache')} />
|
|
289
|
+
<FlowArrow id="flow-cache-event" isAnimating={isFlowAnimating('flow-cache-event')} />
|
|
290
|
+
<StageBox stage={state.stages.event} isAnimating={isStageAnimating('event')} />
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
{/* Stats */}
|
|
294
|
+
<div style={statsStyle}>
|
|
295
|
+
<div style={statBoxStyle}>
|
|
296
|
+
<div style={{ fontSize: 24, fontWeight: 'bold' }}>{state.repos.size}</div>
|
|
297
|
+
<div style={{ fontSize: 12, color: '#9ca3af' }}>Repos</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div style={statBoxStyle}>
|
|
300
|
+
<div style={{ fontSize: 24, fontWeight: 'bold' }}>{state.stages.cache.count}</div>
|
|
301
|
+
<div style={{ fontSize: 12, color: '#9ca3af' }}>Cache Ops</div>
|
|
302
|
+
</div>
|
|
303
|
+
<div style={statBoxStyle}>
|
|
304
|
+
<div style={{ fontSize: 24, fontWeight: 'bold' }}>{state.stages.event.count}</div>
|
|
305
|
+
<div style={{ fontSize: 12, color: '#9ca3af' }}>Events</div>
|
|
306
|
+
</div>
|
|
307
|
+
<div style={statBoxStyle}>
|
|
308
|
+
<div style={{ fontSize: 24, fontWeight: 'bold' }}>{state.totalEvents}</div>
|
|
309
|
+
<div style={{ fontSize: 12, color: '#9ca3af' }}>Total</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{/* Repo List */}
|
|
314
|
+
{state.repos.size > 0 && (
|
|
315
|
+
<div style={repoListStyle}>
|
|
316
|
+
<div style={{ fontSize: 12, color: '#9ca3af', marginBottom: 8 }}>
|
|
317
|
+
Active Repositories
|
|
318
|
+
</div>
|
|
319
|
+
{Array.from(state.repos.values()).map((repo) => (
|
|
320
|
+
<div
|
|
321
|
+
key={repo.path}
|
|
322
|
+
style={{
|
|
323
|
+
display: 'flex',
|
|
324
|
+
justifyContent: 'space-between',
|
|
325
|
+
padding: '4px 0',
|
|
326
|
+
borderBottom: '1px solid #374151',
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
<span style={{ fontSize: 13 }}>{repo.path}</span>
|
|
330
|
+
<span style={{ fontSize: 11, color: '#9ca3af' }}>
|
|
331
|
+
{repo.lastEventType} - {formatTimeAgo(repo.lastEventTime)}
|
|
332
|
+
</span>
|
|
333
|
+
</div>
|
|
334
|
+
))}
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
|
|
338
|
+
{/* CSS for animations */}
|
|
339
|
+
<style>{`
|
|
340
|
+
@keyframes pulse {
|
|
341
|
+
0%, 100% { opacity: 1; }
|
|
342
|
+
50% { opacity: 0.5; }
|
|
343
|
+
}
|
|
344
|
+
`}</style>
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State View Components
|
|
3
|
+
*
|
|
4
|
+
* A pattern for state-driven visualizations where:
|
|
5
|
+
* - Telemetry events are processed into state changes
|
|
6
|
+
* - State has a defined shape
|
|
7
|
+
* - State changes trigger animations/transitions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export * from './types';
|
|
11
|
+
export { useStateView } from './useStateView';
|
|
12
|
+
export type { UseStateViewOptions, UseStateViewResult } from './useStateView';
|
|
13
|
+
export { PipelineView } from './PipelineView';
|
|
14
|
+
export type { PipelineViewProps } from './PipelineView';
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State View Types
|
|
3
|
+
*
|
|
4
|
+
* A state-based visualization system where:
|
|
5
|
+
* - Telemetry events are processed into state changes
|
|
6
|
+
* - State has a defined shape
|
|
7
|
+
* - State changes trigger animations/transitions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Core Event Types
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A state event extracted from telemetry (OTEL spans, etc.)
|
|
16
|
+
*/
|
|
17
|
+
export interface StateEvent<TPayload = Record<string, unknown>> {
|
|
18
|
+
/** Event type identifier */
|
|
19
|
+
type: string;
|
|
20
|
+
/** When this event occurred */
|
|
21
|
+
timestamp: number;
|
|
22
|
+
/** Event-specific data */
|
|
23
|
+
payload: TPayload;
|
|
24
|
+
/** Optional trace correlation */
|
|
25
|
+
traceId?: string;
|
|
26
|
+
spanId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Source of events - can be live (IPC/WebSocket) or replay (from storage)
|
|
31
|
+
*/
|
|
32
|
+
export interface EventSource<TEvent extends StateEvent = StateEvent> {
|
|
33
|
+
/** Subscribe to events, returns unsubscribe function */
|
|
34
|
+
subscribe(handler: (event: TEvent) => void): () => void;
|
|
35
|
+
/** Optional: current mode */
|
|
36
|
+
mode?: 'live' | 'replay';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// State Types
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A reducer that applies events to state
|
|
45
|
+
*/
|
|
46
|
+
export type StateReducer<TState, TEvent extends StateEvent> = (
|
|
47
|
+
state: TState,
|
|
48
|
+
event: TEvent
|
|
49
|
+
) => TState;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Describes what changed between two states (for animations)
|
|
53
|
+
*/
|
|
54
|
+
export interface StateDiff<TState> {
|
|
55
|
+
/** Previous state */
|
|
56
|
+
prev: TState;
|
|
57
|
+
/** New state */
|
|
58
|
+
next: TState;
|
|
59
|
+
/** Which paths changed (dot notation) */
|
|
60
|
+
changedPaths: string[];
|
|
61
|
+
/** The event that caused this change */
|
|
62
|
+
event: StateEvent;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Animation/Transition Types
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
export type AnimationType =
|
|
70
|
+
| 'pulse'
|
|
71
|
+
| 'glow'
|
|
72
|
+
| 'flash'
|
|
73
|
+
| 'shake'
|
|
74
|
+
| 'slide-in'
|
|
75
|
+
| 'slide-out'
|
|
76
|
+
| 'fade-in'
|
|
77
|
+
| 'fade-out'
|
|
78
|
+
| 'particle-flow'
|
|
79
|
+
| 'increment'
|
|
80
|
+
| 'decrement'
|
|
81
|
+
| 'bar-change';
|
|
82
|
+
|
|
83
|
+
export interface TransitionDefinition {
|
|
84
|
+
/** Condition: which state path to watch */
|
|
85
|
+
watch: string;
|
|
86
|
+
/** Condition type */
|
|
87
|
+
condition: 'changed' | 'increased' | 'decreased' | 'added' | 'removed';
|
|
88
|
+
/** Animation to trigger */
|
|
89
|
+
animate: AnimationType;
|
|
90
|
+
/** Target element (CSS selector or element ID) */
|
|
91
|
+
target?: string;
|
|
92
|
+
/** Duration in ms */
|
|
93
|
+
duration?: number;
|
|
94
|
+
/** Additional params */
|
|
95
|
+
params?: Record<string, unknown>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ActiveAnimation {
|
|
99
|
+
id: string;
|
|
100
|
+
type: AnimationType;
|
|
101
|
+
target: string;
|
|
102
|
+
startTime: number;
|
|
103
|
+
duration: number;
|
|
104
|
+
params?: Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Replay Types
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
export interface ReplayControls {
|
|
112
|
+
/** Current playback state */
|
|
113
|
+
state: 'playing' | 'paused' | 'stopped';
|
|
114
|
+
/** Playback speed multiplier */
|
|
115
|
+
speed: number;
|
|
116
|
+
/** Current replay time (event time, not wall time) */
|
|
117
|
+
currentTime: number;
|
|
118
|
+
/** Start of replay range */
|
|
119
|
+
startTime: number;
|
|
120
|
+
/** End of replay range */
|
|
121
|
+
endTime: number;
|
|
122
|
+
/** Control methods */
|
|
123
|
+
play: () => void;
|
|
124
|
+
pause: () => void;
|
|
125
|
+
stop: () => void;
|
|
126
|
+
seek: (time: number) => void;
|
|
127
|
+
setSpeed: (speed: number) => void;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// =============================================================================
|
|
131
|
+
// Pipeline View Specific Types (Case Study 1)
|
|
132
|
+
// =============================================================================
|
|
133
|
+
|
|
134
|
+
export interface PipelineStage {
|
|
135
|
+
id: string;
|
|
136
|
+
label: string;
|
|
137
|
+
lastEventTime: number | null;
|
|
138
|
+
isActive: boolean;
|
|
139
|
+
count?: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface PipelineRepoState {
|
|
143
|
+
path: string;
|
|
144
|
+
lastEventTime: number;
|
|
145
|
+
lastEventType: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface PipelineState {
|
|
149
|
+
stages: Record<string, PipelineStage>;
|
|
150
|
+
repos: Map<string, PipelineRepoState>;
|
|
151
|
+
totalEvents: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type PipelineEventType =
|
|
155
|
+
| 'FS_CHANGE'
|
|
156
|
+
| 'WATCH_DETECTED'
|
|
157
|
+
| 'CACHE_REBUILD'
|
|
158
|
+
| 'CACHE_HIT'
|
|
159
|
+
| 'EVENT_BROADCAST';
|
|
160
|
+
|
|
161
|
+
export interface PipelineEvent extends StateEvent {
|
|
162
|
+
type: PipelineEventType;
|
|
163
|
+
payload: {
|
|
164
|
+
repo?: string;
|
|
165
|
+
stage?: string;
|
|
166
|
+
slice?: string;
|
|
167
|
+
eventType?: string;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// =============================================================================
|
|
172
|
+
// Activity View Specific Types (Case Study 2)
|
|
173
|
+
// =============================================================================
|
|
174
|
+
|
|
175
|
+
export interface RoomState {
|
|
176
|
+
id: string;
|
|
177
|
+
userCount: number;
|
|
178
|
+
users: Set<string>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface ActivityState {
|
|
182
|
+
rooms: Map<string, RoomState>;
|
|
183
|
+
totalUsers: number;
|
|
184
|
+
syncRatePerMinute: number;
|
|
185
|
+
recentSyncs: number[];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type ActivityEventType =
|
|
189
|
+
| 'ROOM_JOIN'
|
|
190
|
+
| 'ROOM_LEAVE'
|
|
191
|
+
| 'SYNC_BROADCAST';
|
|
192
|
+
|
|
193
|
+
export interface ActivityEvent extends StateEvent {
|
|
194
|
+
type: ActivityEventType;
|
|
195
|
+
payload: {
|
|
196
|
+
roomId?: string;
|
|
197
|
+
userId?: string;
|
|
198
|
+
userLogin?: string;
|
|
199
|
+
syncType?: string;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// =============================================================================
|
|
204
|
+
// Quota View Specific Types (Case Study 3)
|
|
205
|
+
// =============================================================================
|
|
206
|
+
|
|
207
|
+
export interface UserQuotaState {
|
|
208
|
+
userId: number;
|
|
209
|
+
userLogin: string;
|
|
210
|
+
remaining: number;
|
|
211
|
+
limit: number;
|
|
212
|
+
resetTime: number;
|
|
213
|
+
callsThisHour: number;
|
|
214
|
+
cacheHits: number;
|
|
215
|
+
byOperation: Map<string, { total: number; apiCalls: number }>;
|
|
216
|
+
byRepo: Map<string, { total: number; apiCalls: number }>;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface QuotaDistributionState {
|
|
220
|
+
users: Map<number, UserQuotaState>;
|
|
221
|
+
serverRemaining: number;
|
|
222
|
+
serverResetTime: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type QuotaEventType = 'GITHUB_REQUEST';
|
|
226
|
+
|
|
227
|
+
export interface QuotaEvent extends StateEvent {
|
|
228
|
+
type: QuotaEventType;
|
|
229
|
+
payload: {
|
|
230
|
+
userId?: number;
|
|
231
|
+
userLogin?: string;
|
|
232
|
+
tokenType: 'user' | 'server';
|
|
233
|
+
operation: string;
|
|
234
|
+
repoOwner?: string;
|
|
235
|
+
repoName?: string;
|
|
236
|
+
cacheHit: boolean;
|
|
237
|
+
cacheTier?: string;
|
|
238
|
+
ratelimitRemaining?: number;
|
|
239
|
+
ratelimitLimit?: number;
|
|
240
|
+
ratelimitReset?: number;
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// =============================================================================
|
|
245
|
+
// View Definition (Schema)
|
|
246
|
+
// =============================================================================
|
|
247
|
+
|
|
248
|
+
export interface StateViewDefinition<TState, TEvent extends StateEvent> {
|
|
249
|
+
id: string;
|
|
250
|
+
name: string;
|
|
251
|
+
description?: string;
|
|
252
|
+
|
|
253
|
+
/** Initial state */
|
|
254
|
+
initialState: TState;
|
|
255
|
+
|
|
256
|
+
/** Reducer function or reference */
|
|
257
|
+
reducer: StateReducer<TState, TEvent>;
|
|
258
|
+
|
|
259
|
+
/** Transition definitions for animations */
|
|
260
|
+
transitions?: TransitionDefinition[];
|
|
261
|
+
}
|