flock-core 0.5.0b65__py3-none-any.whl → 0.5.0b70__py3-none-any.whl
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.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/cli.py +74 -2
- flock/engines/dspy_engine.py +40 -4
- flock/examples.py +4 -1
- flock/frontend/README.md +15 -1
- flock/frontend/package-lock.json +2 -2
- flock/frontend/package.json +1 -1
- flock/frontend/src/App.tsx +74 -6
- flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +4 -5
- flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +7 -3
- flock/frontend/src/components/filters/ArtifactTypeFilter.tsx +21 -0
- flock/frontend/src/components/filters/FilterFlyout.module.css +104 -0
- flock/frontend/src/components/filters/FilterFlyout.tsx +80 -0
- flock/frontend/src/components/filters/FilterPills.module.css +186 -45
- flock/frontend/src/components/filters/FilterPills.test.tsx +115 -99
- flock/frontend/src/components/filters/FilterPills.tsx +120 -44
- flock/frontend/src/components/filters/ProducerFilter.tsx +21 -0
- flock/frontend/src/components/filters/SavedFiltersControl.module.css +60 -0
- flock/frontend/src/components/filters/SavedFiltersControl.test.tsx +158 -0
- flock/frontend/src/components/filters/SavedFiltersControl.tsx +159 -0
- flock/frontend/src/components/filters/TagFilter.tsx +21 -0
- flock/frontend/src/components/filters/TimeRangeFilter.module.css +24 -0
- flock/frontend/src/components/filters/TimeRangeFilter.tsx +6 -1
- flock/frontend/src/components/filters/VisibilityFilter.tsx +21 -0
- flock/frontend/src/components/graph/GraphCanvas.tsx +24 -0
- flock/frontend/src/components/layout/DashboardLayout.css +13 -0
- flock/frontend/src/components/layout/DashboardLayout.tsx +8 -24
- flock/frontend/src/components/modules/HistoricalArtifactsModule.module.css +288 -0
- flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +460 -0
- flock/frontend/src/components/modules/HistoricalArtifactsModuleWrapper.tsx +13 -0
- flock/frontend/src/components/modules/ModuleRegistry.ts +7 -1
- flock/frontend/src/components/modules/registerModules.ts +9 -10
- flock/frontend/src/hooks/useModules.ts +11 -1
- flock/frontend/src/services/api.ts +140 -0
- flock/frontend/src/services/indexeddb.ts +56 -2
- flock/frontend/src/services/websocket.ts +129 -0
- flock/frontend/src/store/filterStore.test.ts +105 -185
- flock/frontend/src/store/filterStore.ts +173 -26
- flock/frontend/src/store/graphStore.test.ts +19 -0
- flock/frontend/src/store/graphStore.ts +166 -27
- flock/frontend/src/types/filters.ts +34 -1
- flock/frontend/src/types/graph.ts +7 -0
- flock/frontend/src/utils/artifacts.ts +24 -0
- flock/orchestrator.py +23 -1
- flock/service.py +146 -9
- flock/store.py +971 -24
- {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/METADATA +26 -1
- {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/RECORD +50 -43
- flock/frontend/src/components/filters/FilterBar.module.css +0 -29
- flock/frontend/src/components/filters/FilterBar.test.tsx +0 -133
- flock/frontend/src/components/filters/FilterBar.tsx +0 -33
- flock/frontend/src/components/modules/EventLogModule.test.tsx +0 -401
- flock/frontend/src/components/modules/EventLogModule.tsx +0 -396
- flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +0 -17
- {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { fetchArtifactSummary, fetchArtifacts, type ArtifactListItem, type ArtifactQueryOptions } from '../../services/api';
|
|
3
|
+
import { mapArtifactToMessage } from '../../utils/artifacts';
|
|
4
|
+
import { useFilterStore } from '../../store/filterStore';
|
|
5
|
+
import { useGraphStore } from '../../store/graphStore';
|
|
6
|
+
import { useUIStore } from '../../store/uiStore';
|
|
7
|
+
import type { ModuleContext } from './ModuleRegistry';
|
|
8
|
+
import JsonAttributeRenderer from './JsonAttributeRenderer';
|
|
9
|
+
import styles from './HistoricalArtifactsModule.module.css';
|
|
10
|
+
|
|
11
|
+
const PAGE_SIZE = 100;
|
|
12
|
+
const ITEM_HEIGHT = 48;
|
|
13
|
+
const MIN_VISIBLE_ITEMS = 8;
|
|
14
|
+
|
|
15
|
+
type TimeRangeSelection = ReturnType<typeof useFilterStore.getState>['timeRange'];
|
|
16
|
+
|
|
17
|
+
type HistoricalArtifactsModuleProps = {
|
|
18
|
+
context: ModuleContext;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const resolveTimeRangeToIso = (range: TimeRangeSelection): { from?: string; to?: string } => {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
if (range.preset === 'last5min') {
|
|
24
|
+
return {
|
|
25
|
+
from: new Date(now - 5 * 60 * 1000).toISOString(),
|
|
26
|
+
to: new Date(now).toISOString(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (range.preset === 'last10min') {
|
|
30
|
+
return {
|
|
31
|
+
from: new Date(now - 10 * 60 * 1000).toISOString(),
|
|
32
|
+
to: new Date(now).toISOString(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (range.preset === 'last1hour') {
|
|
36
|
+
return {
|
|
37
|
+
from: new Date(now - 60 * 60 * 1000).toISOString(),
|
|
38
|
+
to: new Date(now).toISOString(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (range.preset === 'custom' && range.start && range.end) {
|
|
42
|
+
return {
|
|
43
|
+
from: new Date(range.start).toISOString(),
|
|
44
|
+
to: new Date(range.end).toISOString(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const HistoricalArtifactsModule: React.FC<HistoricalArtifactsModuleProps> = ({ context }) => {
|
|
51
|
+
// Context reserved for future extensions (module lifecycle expects prop)
|
|
52
|
+
void context;
|
|
53
|
+
const correlationId = useFilterStore((state) => state.correlationId);
|
|
54
|
+
const timeRange = useFilterStore((state) => state.timeRange);
|
|
55
|
+
const selectedArtifactTypes = useFilterStore((state) => state.selectedArtifactTypes);
|
|
56
|
+
const selectedProducers = useFilterStore((state) => state.selectedProducers);
|
|
57
|
+
const selectedTags = useFilterStore((state) => state.selectedTags);
|
|
58
|
+
const selectedVisibility = useFilterStore((state) => state.selectedVisibility);
|
|
59
|
+
const setSummary = useFilterStore((state) => state.setSummary);
|
|
60
|
+
const updateAvailableCorrelationIds = useFilterStore((state) => state.updateAvailableCorrelationIds);
|
|
61
|
+
const summary = useFilterStore((state) => state.summary);
|
|
62
|
+
|
|
63
|
+
const [artifacts, setArtifacts] = useState<ArtifactListItem[]>([]);
|
|
64
|
+
const [nextOffset, setNextOffset] = useState(0);
|
|
65
|
+
const [total, setTotal] = useState(0);
|
|
66
|
+
const [loading, setLoading] = useState(false);
|
|
67
|
+
const [error, setError] = useState<string | null>(null);
|
|
68
|
+
const [selectedArtifactId, setSelectedArtifactId] = useState<string | null>(null);
|
|
69
|
+
|
|
70
|
+
const lastAutoLoadIndex = useRef(-1);
|
|
71
|
+
const virtualScrollRef = useRef<HTMLDivElement | null>(null);
|
|
72
|
+
const [virtualRange, setVirtualRange] = useState({ start: 0, end: MIN_VISIBLE_ITEMS });
|
|
73
|
+
|
|
74
|
+
const selectedArtifact = useMemo(
|
|
75
|
+
() => artifacts.find((artifact) => artifact.id === selectedArtifactId) ?? null,
|
|
76
|
+
[artifacts, selectedArtifactId]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const hasMore = total > nextOffset;
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (selectedArtifactId && !artifacts.some((artifact) => artifact.id === selectedArtifactId)) {
|
|
83
|
+
setSelectedArtifactId(null);
|
|
84
|
+
}
|
|
85
|
+
}, [artifacts, selectedArtifactId]);
|
|
86
|
+
|
|
87
|
+
const buildQueryOptions = useCallback(
|
|
88
|
+
(offset: number): ArtifactQueryOptions => {
|
|
89
|
+
const range = resolveTimeRangeToIso(timeRange);
|
|
90
|
+
return {
|
|
91
|
+
types: selectedArtifactTypes,
|
|
92
|
+
producers: selectedProducers,
|
|
93
|
+
tags: selectedTags,
|
|
94
|
+
visibility: selectedVisibility,
|
|
95
|
+
correlationId,
|
|
96
|
+
from: range.from,
|
|
97
|
+
to: range.to,
|
|
98
|
+
limit: PAGE_SIZE,
|
|
99
|
+
offset,
|
|
100
|
+
embedMeta: true,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
[timeRange, selectedArtifactTypes, selectedProducers, selectedTags, selectedVisibility, correlationId]
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const mergeCorrelationMetadata = useCallback(
|
|
107
|
+
(items: ArtifactListItem[]) => {
|
|
108
|
+
if (items.length === 0) return;
|
|
109
|
+
|
|
110
|
+
const existing = useFilterStore.getState().availableCorrelationIds;
|
|
111
|
+
const merged = new Map(existing.map((item) => [item.correlation_id, { ...item }]));
|
|
112
|
+
|
|
113
|
+
items.forEach((item) => {
|
|
114
|
+
if (!item.correlation_id) return;
|
|
115
|
+
const timestamp = new Date(item.created_at).getTime();
|
|
116
|
+
const current = merged.get(item.correlation_id);
|
|
117
|
+
if (current) {
|
|
118
|
+
current.artifact_count += 1;
|
|
119
|
+
current.first_seen = Math.min(current.first_seen, timestamp);
|
|
120
|
+
} else {
|
|
121
|
+
merged.set(item.correlation_id, {
|
|
122
|
+
correlation_id: item.correlation_id,
|
|
123
|
+
first_seen: timestamp,
|
|
124
|
+
artifact_count: 1,
|
|
125
|
+
run_count: 0,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
updateAvailableCorrelationIds(Array.from(merged.values()));
|
|
131
|
+
},
|
|
132
|
+
[updateAvailableCorrelationIds]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const loadArtifacts = useCallback(
|
|
136
|
+
async (reset: boolean) => {
|
|
137
|
+
setLoading(true);
|
|
138
|
+
try {
|
|
139
|
+
const offset = reset ? 0 : nextOffset;
|
|
140
|
+
const queryOptions = buildQueryOptions(offset);
|
|
141
|
+
const response = await fetchArtifacts(queryOptions);
|
|
142
|
+
|
|
143
|
+
setArtifacts((prev) => (reset ? response.items : [...prev, ...response.items]));
|
|
144
|
+
setNextOffset(offset + response.pagination.limit);
|
|
145
|
+
setTotal(response.pagination.total);
|
|
146
|
+
setError(null);
|
|
147
|
+
|
|
148
|
+
mergeCorrelationMetadata(response.items);
|
|
149
|
+
|
|
150
|
+
if (response.items.length > 0) {
|
|
151
|
+
const graphStore = useGraphStore.getState();
|
|
152
|
+
graphStore.batchUpdate({ messages: response.items.map(mapArtifactToMessage) });
|
|
153
|
+
const uiState = useUIStore.getState();
|
|
154
|
+
if (uiState.mode === 'agent') {
|
|
155
|
+
graphStore.generateAgentViewGraph();
|
|
156
|
+
} else {
|
|
157
|
+
graphStore.generateBlackboardViewGraph();
|
|
158
|
+
}
|
|
159
|
+
graphStore.applyFilters();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const summaryResponse = await fetchArtifactSummary({
|
|
163
|
+
...queryOptions,
|
|
164
|
+
limit: undefined,
|
|
165
|
+
offset: undefined,
|
|
166
|
+
});
|
|
167
|
+
setSummary(summaryResponse);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error('[HistoricalArtifactsModule] Failed to load artifacts', err);
|
|
170
|
+
setError('Failed to load artifacts');
|
|
171
|
+
} finally {
|
|
172
|
+
setLoading(false);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
[buildQueryOptions, mergeCorrelationMetadata, nextOffset, setSummary]
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
lastAutoLoadIndex.current = -1;
|
|
180
|
+
loadArtifacts(true);
|
|
181
|
+
}, [loadArtifacts]);
|
|
182
|
+
|
|
183
|
+
const rows = useMemo(
|
|
184
|
+
() =>
|
|
185
|
+
artifacts.map((artifact) => ({
|
|
186
|
+
id: artifact.id,
|
|
187
|
+
timestamp: new Date(artifact.created_at).toLocaleString(),
|
|
188
|
+
type: artifact.type,
|
|
189
|
+
producedBy: artifact.produced_by,
|
|
190
|
+
correlationId: artifact.correlation_id ?? '—',
|
|
191
|
+
tags: artifact.tags.join(', ') || '—',
|
|
192
|
+
visibility: artifact.visibility_kind || artifact.visibility?.kind || 'Unknown',
|
|
193
|
+
consumedCount: artifact.consumptions?.length ?? 0,
|
|
194
|
+
})),
|
|
195
|
+
[artifacts]
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const handleLoadMore = () => {
|
|
199
|
+
if (!loading && hasMore) {
|
|
200
|
+
lastAutoLoadIndex.current = -1;
|
|
201
|
+
loadArtifacts(false);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const listHeight = useMemo(() => Math.max(MIN_VISIBLE_ITEMS, Math.min(rows.length || MIN_VISIBLE_ITEMS, 12)) * ITEM_HEIGHT, [rows.length]);
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
const container = virtualScrollRef.current;
|
|
208
|
+
if (!container) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const handleScroll = () => {
|
|
213
|
+
const totalItems = rows.length;
|
|
214
|
+
if (totalItems === 0) {
|
|
215
|
+
setVirtualRange({ start: 0, end: MIN_VISIBLE_ITEMS });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const scrollTop = container.scrollTop;
|
|
220
|
+
const viewportHeight = container.clientHeight;
|
|
221
|
+
const startIndex = Math.max(0, Math.floor(scrollTop / ITEM_HEIGHT) - 5);
|
|
222
|
+
const endIndex = Math.min(totalItems, Math.ceil((scrollTop + viewportHeight) / ITEM_HEIGHT) + 5);
|
|
223
|
+
setVirtualRange({ start: startIndex, end: endIndex });
|
|
224
|
+
|
|
225
|
+
if (hasMore && !loading && endIndex >= totalItems - 5 && lastAutoLoadIndex.current !== endIndex) {
|
|
226
|
+
lastAutoLoadIndex.current = endIndex;
|
|
227
|
+
handleLoadMore();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
handleScroll();
|
|
232
|
+
container.addEventListener('scroll', handleScroll);
|
|
233
|
+
return () => container.removeEventListener('scroll', handleScroll);
|
|
234
|
+
}, [handleLoadMore, hasMore, loading, rows.length]);
|
|
235
|
+
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
const container = virtualScrollRef.current;
|
|
238
|
+
if (container) {
|
|
239
|
+
container.scrollTop = 0;
|
|
240
|
+
}
|
|
241
|
+
setVirtualRange({ start: 0, end: Math.max(MIN_VISIBLE_ITEMS, Math.min(rows.length, MIN_VISIBLE_ITEMS * 2)) });
|
|
242
|
+
lastAutoLoadIndex.current = -1;
|
|
243
|
+
}, [rows.length]);
|
|
244
|
+
|
|
245
|
+
const retentionInfo = useMemo(() => {
|
|
246
|
+
if (!summary?.earliest_created_at || !summary?.latest_created_at) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
const earliest = new Date(summary.earliest_created_at);
|
|
250
|
+
const latest = new Date(summary.latest_created_at);
|
|
251
|
+
const spanMs = Math.max(0, latest.getTime() - earliest.getTime());
|
|
252
|
+
const spanDays = spanMs / (1000 * 60 * 60 * 24);
|
|
253
|
+
let spanLabel: string;
|
|
254
|
+
if (spanDays >= 2) {
|
|
255
|
+
spanLabel = `${spanDays.toFixed(1)} days`;
|
|
256
|
+
} else if (spanDays >= 0.5) {
|
|
257
|
+
spanLabel = `${(spanDays * 24).toFixed(0)} hours`;
|
|
258
|
+
} else {
|
|
259
|
+
spanLabel = `${Math.max(1, Math.round(spanMs / (1000 * 60)))} minutes`;
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
earliest: earliest.toLocaleString(),
|
|
263
|
+
latest: latest.toLocaleString(),
|
|
264
|
+
spanLabel,
|
|
265
|
+
};
|
|
266
|
+
}, [summary]);
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<div className={styles.container}>
|
|
270
|
+
<header className={styles.header}>
|
|
271
|
+
<div className={styles.metrics}>
|
|
272
|
+
<div>
|
|
273
|
+
<span className={styles.metricLabel}>Artifacts</span>
|
|
274
|
+
<span className={styles.metricValue}>{total}</span>
|
|
275
|
+
</div>
|
|
276
|
+
<div>
|
|
277
|
+
<span className={styles.metricLabel}>Earliest</span>
|
|
278
|
+
<span className={styles.metricValue}>
|
|
279
|
+
{summary?.earliest_created_at ? new Date(summary.earliest_created_at).toLocaleString() : '—'}
|
|
280
|
+
</span>
|
|
281
|
+
</div>
|
|
282
|
+
<div>
|
|
283
|
+
<span className={styles.metricLabel}>Latest</span>
|
|
284
|
+
<span className={styles.metricValue}>
|
|
285
|
+
{summary?.latest_created_at ? new Date(summary.latest_created_at).toLocaleString() : '—'}
|
|
286
|
+
</span>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
<div className={styles.actions}>
|
|
290
|
+
<button type="button" onClick={() => loadArtifacts(true)} disabled={loading}>
|
|
291
|
+
Refresh
|
|
292
|
+
</button>
|
|
293
|
+
<button type="button" onClick={handleLoadMore} disabled={loading || !hasMore}>
|
|
294
|
+
Load Older
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
297
|
+
</header>
|
|
298
|
+
|
|
299
|
+
{retentionInfo && (
|
|
300
|
+
<div className={styles.retentionBanner}>
|
|
301
|
+
<span>
|
|
302
|
+
Historical window: <strong>{retentionInfo.spanLabel}</strong> (oldest artifact {retentionInfo.earliest})
|
|
303
|
+
</span>
|
|
304
|
+
<span>
|
|
305
|
+
Latest artifact recorded at <strong>{retentionInfo.latest}</strong>.{' '}
|
|
306
|
+
{hasMore ? 'Use “Load Older” to fetch additional retained history.' : 'You are viewing the full retained history.'}
|
|
307
|
+
</span>
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
|
|
311
|
+
{error && <div className={styles.error}>{error}</div>}
|
|
312
|
+
|
|
313
|
+
{!loading && artifacts.length === 0 && !error && (
|
|
314
|
+
<div className={styles.emptyState}>No artifacts found for current filters.</div>
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
{artifacts.length > 0 && (
|
|
318
|
+
<div className={styles.contentArea}>
|
|
319
|
+
<div className={styles.tableContainer}>
|
|
320
|
+
<div className={styles.headerRow}>
|
|
321
|
+
<span>Timestamp</span>
|
|
322
|
+
<span>Type</span>
|
|
323
|
+
<span>Produced By</span>
|
|
324
|
+
<span>Correlation ID</span>
|
|
325
|
+
<span>Tags</span>
|
|
326
|
+
<span>Visibility</span>
|
|
327
|
+
<span>Consumed</span>
|
|
328
|
+
</div>
|
|
329
|
+
<div
|
|
330
|
+
ref={virtualScrollRef}
|
|
331
|
+
className={styles.virtualViewport}
|
|
332
|
+
style={{ height: listHeight }}
|
|
333
|
+
>
|
|
334
|
+
<div style={{ height: rows.length * ITEM_HEIGHT, position: 'relative' }}>
|
|
335
|
+
<div
|
|
336
|
+
style={{
|
|
337
|
+
position: 'absolute',
|
|
338
|
+
top: virtualRange.start * ITEM_HEIGHT,
|
|
339
|
+
left: 0,
|
|
340
|
+
right: 0,
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
{rows.slice(virtualRange.start, virtualRange.end).map((row, idx) => {
|
|
344
|
+
const absoluteIndex = virtualRange.start + idx;
|
|
345
|
+
const isSelected = row.id === selectedArtifactId;
|
|
346
|
+
const classes = [styles.dataRow];
|
|
347
|
+
if (absoluteIndex % 2 === 1) {
|
|
348
|
+
classes.push(styles.dataRowStripe);
|
|
349
|
+
}
|
|
350
|
+
if (isSelected) {
|
|
351
|
+
classes.push(styles.dataRowSelected);
|
|
352
|
+
}
|
|
353
|
+
return (
|
|
354
|
+
<div
|
|
355
|
+
key={row.id}
|
|
356
|
+
className={classes.join(' ')}
|
|
357
|
+
style={{ height: ITEM_HEIGHT }}
|
|
358
|
+
role="button"
|
|
359
|
+
tabIndex={0}
|
|
360
|
+
aria-selected={isSelected}
|
|
361
|
+
onClick={() => setSelectedArtifactId(row.id)}
|
|
362
|
+
onKeyDown={(event) => {
|
|
363
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
setSelectedArtifactId(row.id);
|
|
366
|
+
}
|
|
367
|
+
}}
|
|
368
|
+
>
|
|
369
|
+
<span>{row.timestamp}</span>
|
|
370
|
+
<span>{row.type}</span>
|
|
371
|
+
<span>{row.producedBy}</span>
|
|
372
|
+
<span>{row.correlationId}</span>
|
|
373
|
+
<span>{row.tags}</span>
|
|
374
|
+
<span>{row.visibility}</span>
|
|
375
|
+
<span>{row.consumedCount}</span>
|
|
376
|
+
</div>
|
|
377
|
+
);
|
|
378
|
+
})}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
<aside className={styles.detailPanel}>
|
|
384
|
+
{selectedArtifact ? (
|
|
385
|
+
<>
|
|
386
|
+
<div className={styles.detailHeader}>
|
|
387
|
+
<div>
|
|
388
|
+
<h3>{selectedArtifact.type}</h3>
|
|
389
|
+
<p>{new Date(selectedArtifact.created_at).toLocaleString()}</p>
|
|
390
|
+
</div>
|
|
391
|
+
<button type="button" onClick={() => setSelectedArtifactId(null)}>
|
|
392
|
+
Clear
|
|
393
|
+
</button>
|
|
394
|
+
</div>
|
|
395
|
+
<div className={styles.detailSection}>
|
|
396
|
+
<h4>Metadata</h4>
|
|
397
|
+
<dl className={styles.detailList}>
|
|
398
|
+
<div>
|
|
399
|
+
<dt>Produced By</dt>
|
|
400
|
+
<dd>{selectedArtifact.produced_by}</dd>
|
|
401
|
+
</div>
|
|
402
|
+
<div>
|
|
403
|
+
<dt>Correlation</dt>
|
|
404
|
+
<dd>{selectedArtifact.correlation_id || '—'}</dd>
|
|
405
|
+
</div>
|
|
406
|
+
<div>
|
|
407
|
+
<dt>Partition</dt>
|
|
408
|
+
<dd>{selectedArtifact.partition_key || '—'}</dd>
|
|
409
|
+
</div>
|
|
410
|
+
<div>
|
|
411
|
+
<dt>Tags</dt>
|
|
412
|
+
<dd>{selectedArtifact.tags.length ? selectedArtifact.tags.join(', ') : '—'}</dd>
|
|
413
|
+
</div>
|
|
414
|
+
<div>
|
|
415
|
+
<dt>Visibility</dt>
|
|
416
|
+
<dd>{selectedArtifact.visibility_kind || selectedArtifact.visibility?.kind || 'Unknown'}</dd>
|
|
417
|
+
</div>
|
|
418
|
+
<div>
|
|
419
|
+
<dt>Consumed By</dt>
|
|
420
|
+
<dd>{selectedArtifact.consumed_by?.length ? selectedArtifact.consumed_by.join(', ') : '—'}</dd>
|
|
421
|
+
</div>
|
|
422
|
+
</dl>
|
|
423
|
+
</div>
|
|
424
|
+
<div className={styles.detailSection}>
|
|
425
|
+
<h4>Payload</h4>
|
|
426
|
+
<JsonAttributeRenderer
|
|
427
|
+
value={JSON.stringify(selectedArtifact.payload, null, 2)}
|
|
428
|
+
maxStringLength={Number.POSITIVE_INFINITY}
|
|
429
|
+
/>
|
|
430
|
+
</div>
|
|
431
|
+
<div className={styles.detailSection}>
|
|
432
|
+
<h4>Consumption History</h4>
|
|
433
|
+
{selectedArtifact.consumptions && selectedArtifact.consumptions.length > 0 ? (
|
|
434
|
+
<ul className={styles.consumptionList}>
|
|
435
|
+
{selectedArtifact.consumptions.map((entry) => (
|
|
436
|
+
<li key={`${entry.consumer}-${entry.consumed_at}`}>
|
|
437
|
+
<span className={styles.consumerName}>{entry.consumer}</span>
|
|
438
|
+
<span>{new Date(entry.consumed_at).toLocaleString()}</span>
|
|
439
|
+
{entry.run_id ? <span className={styles.runBadge}>Run {entry.run_id}</span> : null}
|
|
440
|
+
</li>
|
|
441
|
+
))}
|
|
442
|
+
</ul>
|
|
443
|
+
) : (
|
|
444
|
+
<p className={styles.emptyConsumption}>No consumers recorded for this artifact.</p>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
</>
|
|
448
|
+
) : (
|
|
449
|
+
<div className={styles.emptyDetail}>Select an artifact to inspect payload and history.</div>
|
|
450
|
+
)}
|
|
451
|
+
</aside>
|
|
452
|
+
</div>
|
|
453
|
+
)}
|
|
454
|
+
|
|
455
|
+
{loading && <div className={styles.loading}>Loading…</div>}
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
export default HistoricalArtifactsModule;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ModuleContext } from './ModuleRegistry';
|
|
3
|
+
import HistoricalArtifactsModule from './HistoricalArtifactsModule';
|
|
4
|
+
|
|
5
|
+
interface HistoricalArtifactsModuleWrapperProps {
|
|
6
|
+
context: ModuleContext;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const HistoricalArtifactsModuleWrapper: React.FC<HistoricalArtifactsModuleWrapperProps> = ({ context }) => {
|
|
10
|
+
return <HistoricalArtifactsModule context={context} />;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default HistoricalArtifactsModuleWrapper;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { Agent, Message } from '../../types/graph';
|
|
3
|
-
import type { TimeRange } from '../../types/filters';
|
|
3
|
+
import type { TimeRange, ArtifactSummary } from '../../types/filters';
|
|
4
4
|
|
|
5
5
|
export interface ModuleContext {
|
|
6
6
|
// Data access
|
|
@@ -12,8 +12,14 @@ export interface ModuleContext {
|
|
|
12
12
|
filters: {
|
|
13
13
|
correlationId: string | null;
|
|
14
14
|
timeRange: TimeRange;
|
|
15
|
+
artifactTypes: string[];
|
|
16
|
+
producers: string[];
|
|
17
|
+
tags: string[];
|
|
18
|
+
visibility: string[];
|
|
15
19
|
};
|
|
16
20
|
|
|
21
|
+
summary: ArtifactSummary | null;
|
|
22
|
+
|
|
17
23
|
// Actions
|
|
18
24
|
publish: (artifact: any) => void;
|
|
19
25
|
invoke: (agentName: string, inputs: any[]) => void;
|
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
import { moduleRegistry } from './ModuleRegistry';
|
|
2
|
-
import EventLogModuleWrapper from './EventLogModuleWrapper';
|
|
3
2
|
import TraceModuleJaegerWrapper from './TraceModuleJaegerWrapper';
|
|
3
|
+
import HistoricalArtifactsModuleWrapper from './HistoricalArtifactsModuleWrapper';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Register all available modules
|
|
7
7
|
* This should be called during application initialization
|
|
8
8
|
*/
|
|
9
9
|
export function registerModules(): void {
|
|
10
|
-
// Register EventLog module
|
|
11
|
-
moduleRegistry.register({
|
|
12
|
-
id: 'eventLog',
|
|
13
|
-
name: 'Event Log',
|
|
14
|
-
description: 'View and filter system events',
|
|
15
|
-
icon: '📋',
|
|
16
|
-
component: EventLogModuleWrapper,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
10
|
// Register Trace Viewer with Timeline, Statistics, RED Metrics, and Dependencies
|
|
20
11
|
moduleRegistry.register({
|
|
21
12
|
id: 'traceViewerJaeger',
|
|
@@ -25,6 +16,14 @@ export function registerModules(): void {
|
|
|
25
16
|
component: TraceModuleJaegerWrapper,
|
|
26
17
|
});
|
|
27
18
|
|
|
19
|
+
moduleRegistry.register({
|
|
20
|
+
id: 'historicalArtifacts',
|
|
21
|
+
name: 'Historical Blackboard',
|
|
22
|
+
description: 'Browse persisted artifacts and retention metrics',
|
|
23
|
+
icon: '📚',
|
|
24
|
+
component: HistoricalArtifactsModuleWrapper,
|
|
25
|
+
});
|
|
26
|
+
|
|
28
27
|
// Future modules can be registered here
|
|
29
28
|
// moduleRegistry.register({ ... });
|
|
30
29
|
}
|
|
@@ -39,6 +39,11 @@ export function useModules() {
|
|
|
39
39
|
const events = useGraphStore((state) => state.events);
|
|
40
40
|
const correlationId = useFilterStore((state) => state.correlationId);
|
|
41
41
|
const timeRange = useFilterStore((state) => state.timeRange);
|
|
42
|
+
const artifactTypes = useFilterStore((state) => state.selectedArtifactTypes);
|
|
43
|
+
const producers = useFilterStore((state) => state.selectedProducers);
|
|
44
|
+
const tags = useFilterStore((state) => state.selectedTags);
|
|
45
|
+
const visibility = useFilterStore((state) => state.selectedVisibility);
|
|
46
|
+
const summary = useFilterStore((state) => state.summary);
|
|
42
47
|
|
|
43
48
|
// Track previous instances to detect changes
|
|
44
49
|
const prevInstancesRef = useRef<Map<string, any>>(new Map());
|
|
@@ -52,7 +57,12 @@ export function useModules() {
|
|
|
52
57
|
filters: {
|
|
53
58
|
correlationId,
|
|
54
59
|
timeRange,
|
|
60
|
+
artifactTypes,
|
|
61
|
+
producers,
|
|
62
|
+
tags,
|
|
63
|
+
visibility,
|
|
55
64
|
},
|
|
65
|
+
summary,
|
|
56
66
|
publish: (artifact: any) => {
|
|
57
67
|
// Placeholder: In production, this would dispatch to WebSocket
|
|
58
68
|
console.log('[Module Context] Publish artifact:', artifact);
|
|
@@ -62,7 +72,7 @@ export function useModules() {
|
|
|
62
72
|
console.log('[Module Context] Invoke agent:', agentName, 'with inputs:', inputs);
|
|
63
73
|
},
|
|
64
74
|
}),
|
|
65
|
-
[agents, messages, events, correlationId, timeRange]
|
|
75
|
+
[agents, messages, events, correlationId, timeRange, artifactTypes, producers, tags, visibility, summary]
|
|
66
76
|
);
|
|
67
77
|
|
|
68
78
|
/**
|