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.

Files changed (56) hide show
  1. flock/cli.py +74 -2
  2. flock/engines/dspy_engine.py +40 -4
  3. flock/examples.py +4 -1
  4. flock/frontend/README.md +15 -1
  5. flock/frontend/package-lock.json +2 -2
  6. flock/frontend/package.json +1 -1
  7. flock/frontend/src/App.tsx +74 -6
  8. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +4 -5
  9. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +7 -3
  10. flock/frontend/src/components/filters/ArtifactTypeFilter.tsx +21 -0
  11. flock/frontend/src/components/filters/FilterFlyout.module.css +104 -0
  12. flock/frontend/src/components/filters/FilterFlyout.tsx +80 -0
  13. flock/frontend/src/components/filters/FilterPills.module.css +186 -45
  14. flock/frontend/src/components/filters/FilterPills.test.tsx +115 -99
  15. flock/frontend/src/components/filters/FilterPills.tsx +120 -44
  16. flock/frontend/src/components/filters/ProducerFilter.tsx +21 -0
  17. flock/frontend/src/components/filters/SavedFiltersControl.module.css +60 -0
  18. flock/frontend/src/components/filters/SavedFiltersControl.test.tsx +158 -0
  19. flock/frontend/src/components/filters/SavedFiltersControl.tsx +159 -0
  20. flock/frontend/src/components/filters/TagFilter.tsx +21 -0
  21. flock/frontend/src/components/filters/TimeRangeFilter.module.css +24 -0
  22. flock/frontend/src/components/filters/TimeRangeFilter.tsx +6 -1
  23. flock/frontend/src/components/filters/VisibilityFilter.tsx +21 -0
  24. flock/frontend/src/components/graph/GraphCanvas.tsx +24 -0
  25. flock/frontend/src/components/layout/DashboardLayout.css +13 -0
  26. flock/frontend/src/components/layout/DashboardLayout.tsx +8 -24
  27. flock/frontend/src/components/modules/HistoricalArtifactsModule.module.css +288 -0
  28. flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +460 -0
  29. flock/frontend/src/components/modules/HistoricalArtifactsModuleWrapper.tsx +13 -0
  30. flock/frontend/src/components/modules/ModuleRegistry.ts +7 -1
  31. flock/frontend/src/components/modules/registerModules.ts +9 -10
  32. flock/frontend/src/hooks/useModules.ts +11 -1
  33. flock/frontend/src/services/api.ts +140 -0
  34. flock/frontend/src/services/indexeddb.ts +56 -2
  35. flock/frontend/src/services/websocket.ts +129 -0
  36. flock/frontend/src/store/filterStore.test.ts +105 -185
  37. flock/frontend/src/store/filterStore.ts +173 -26
  38. flock/frontend/src/store/graphStore.test.ts +19 -0
  39. flock/frontend/src/store/graphStore.ts +166 -27
  40. flock/frontend/src/types/filters.ts +34 -1
  41. flock/frontend/src/types/graph.ts +7 -0
  42. flock/frontend/src/utils/artifacts.ts +24 -0
  43. flock/orchestrator.py +23 -1
  44. flock/service.py +146 -9
  45. flock/store.py +971 -24
  46. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/METADATA +26 -1
  47. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/RECORD +50 -43
  48. flock/frontend/src/components/filters/FilterBar.module.css +0 -29
  49. flock/frontend/src/components/filters/FilterBar.test.tsx +0 -133
  50. flock/frontend/src/components/filters/FilterBar.tsx +0 -33
  51. flock/frontend/src/components/modules/EventLogModule.test.tsx +0 -401
  52. flock/frontend/src/components/modules/EventLogModule.tsx +0 -396
  53. flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +0 -17
  54. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/WHEEL +0 -0
  55. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/entry_points.txt +0 -0
  56. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/licenses/LICENSE +0 -0
@@ -1,242 +1,162 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { useFilterStore } from './filterStore';
3
+ import type { FilterFacets, FilterSnapshot } from '../types/filters';
3
4
 
4
5
  describe('filterStore', () => {
5
6
  beforeEach(() => {
6
- const store = useFilterStore.getState();
7
- store.clearFilters();
8
- store.updateAvailableCorrelationIds([]);
7
+ useFilterStore.setState({
8
+ correlationId: null,
9
+ timeRange: { preset: 'last10min' },
10
+ selectedArtifactTypes: [],
11
+ selectedProducers: [],
12
+ selectedTags: [],
13
+ selectedVisibility: [],
14
+ availableCorrelationIds: [],
15
+ availableArtifactTypes: [],
16
+ availableProducers: [],
17
+ availableTags: [],
18
+ availableVisibility: [],
19
+ summary: null,
20
+ savedFilters: [],
21
+ });
9
22
  });
10
23
 
11
- describe('Initial State', () => {
12
- it('should have no correlation ID selected', () => {
24
+ describe('initial state', () => {
25
+ it('should have defaults set', () => {
13
26
  const state = useFilterStore.getState();
14
27
  expect(state.correlationId).toBeNull();
15
- });
16
-
17
- it('should have default time range of last 10 minutes', () => {
18
- const state = useFilterStore.getState();
19
28
  expect(state.timeRange).toEqual({ preset: 'last10min' });
20
- });
21
-
22
- it('should have empty available correlation IDs', () => {
23
- const state = useFilterStore.getState();
29
+ expect(state.selectedArtifactTypes).toEqual([]);
24
30
  expect(state.availableCorrelationIds).toEqual([]);
25
31
  });
26
32
  });
27
33
 
28
- describe('setCorrelationId', () => {
29
- it('should set correlation ID', () => {
30
- const store = useFilterStore.getState();
31
- store.setCorrelationId('test-correlation-id');
32
-
33
- expect(useFilterStore.getState().correlationId).toBe('test-correlation-id');
34
- });
35
-
36
- it('should clear correlation ID when set to null', () => {
37
- const store = useFilterStore.getState();
38
- store.setCorrelationId('test-id');
39
- store.setCorrelationId(null);
40
-
41
- expect(useFilterStore.getState().correlationId).toBeNull();
42
- });
43
- });
44
-
45
- describe('setTimeRange', () => {
46
- it('should set time range preset', () => {
34
+ describe('mutators', () => {
35
+ it('should update correlation id and time range', () => {
47
36
  const store = useFilterStore.getState();
37
+ store.setCorrelationId('abc');
48
38
  store.setTimeRange({ preset: 'last5min' });
49
-
39
+ expect(useFilterStore.getState().correlationId).toBe('abc');
50
40
  expect(useFilterStore.getState().timeRange).toEqual({ preset: 'last5min' });
51
41
  });
52
42
 
53
- it('should set custom time range', () => {
43
+ it('should update multi-select filters', () => {
54
44
  const store = useFilterStore.getState();
55
- const customRange = {
56
- preset: 'custom' as const,
57
- start: Date.now() - 3600000,
58
- end: Date.now(),
59
- };
60
- store.setTimeRange(customRange);
45
+ store.setArtifactTypes(['TypeB', 'TypeA']);
46
+ store.setProducers(['b', 'a']);
47
+ store.setTags(['beta']);
48
+ store.setVisibility(['Private', 'Public']);
61
49
 
62
- expect(useFilterStore.getState().timeRange).toEqual(customRange);
50
+ const state = useFilterStore.getState();
51
+ expect(state.selectedArtifactTypes).toEqual(['TypeA', 'TypeB']);
52
+ expect(state.selectedProducers).toEqual(['a', 'b']);
53
+ expect(state.selectedTags).toEqual(['beta']);
54
+ expect(state.selectedVisibility).toEqual(['Private', 'Public']);
63
55
  });
64
- });
65
56
 
66
- describe('clearFilters', () => {
67
- it('should reset all filters to defaults', () => {
57
+ it('should clear filters to defaults', () => {
68
58
  const store = useFilterStore.getState();
69
- store.setCorrelationId('test-id');
70
- store.setTimeRange({ preset: 'last1hour' });
71
-
59
+ store.setCorrelationId('abc');
60
+ store.setArtifactTypes(['TypeA']);
61
+ store.setTags(['x']);
72
62
  store.clearFilters();
73
63
 
74
64
  const state = useFilterStore.getState();
75
65
  expect(state.correlationId).toBeNull();
66
+ expect(state.selectedArtifactTypes).toEqual([]);
67
+ expect(state.selectedTags).toEqual([]);
76
68
  expect(state.timeRange).toEqual({ preset: 'last10min' });
77
69
  });
78
- });
79
70
 
80
- describe('updateAvailableCorrelationIds', () => {
81
- it('should update available correlation IDs with metadata', () => {
82
- const store = useFilterStore.getState();
71
+ it('should update available correlation ids', () => {
83
72
  const now = Date.now();
84
- const metadata = [
85
- {
86
- correlation_id: 'abc123',
87
- first_seen: now - 120000,
88
- artifact_count: 5,
89
- run_count: 2,
90
- },
91
- {
92
- correlation_id: 'def456',
93
- first_seen: now - 60000,
94
- artifact_count: 3,
95
- run_count: 1,
96
- },
97
- ];
98
-
99
- store.updateAvailableCorrelationIds(metadata);
100
-
101
- const state = useFilterStore.getState();
102
- // Should be sorted by most recent first
103
- expect(state.availableCorrelationIds).toHaveLength(2);
104
- expect(state.availableCorrelationIds[0]?.correlation_id).toBe('def456');
105
- expect(state.availableCorrelationIds[1]?.correlation_id).toBe('abc123');
106
- });
107
-
108
- it('should limit to 50 correlation IDs', () => {
109
- const store = useFilterStore.getState();
110
- const metadata = Array.from({ length: 100 }, (_, i) => ({
111
- correlation_id: `id-${i}`,
112
- first_seen: Date.now() - i * 1000,
113
- artifact_count: i,
114
- run_count: 1,
115
- }));
116
-
117
- store.updateAvailableCorrelationIds(metadata);
118
-
73
+ useFilterStore
74
+ .getState()
75
+ .updateAvailableCorrelationIds([
76
+ { correlation_id: 'old', first_seen: now - 10_000, artifact_count: 1, run_count: 1 },
77
+ { correlation_id: 'new', first_seen: now - 1_000, artifact_count: 2, run_count: 2 },
78
+ ]);
119
79
  const state = useFilterStore.getState();
120
- expect(state.availableCorrelationIds).toHaveLength(50);
80
+ expect(state.availableCorrelationIds.map((m) => m.correlation_id)).toEqual(['new', 'old']);
121
81
  });
122
82
 
123
- it('should sort by most recent first', () => {
124
- const store = useFilterStore.getState();
125
- const now = Date.now();
126
- const metadata = [
127
- {
128
- correlation_id: 'oldest',
129
- first_seen: now - 300000,
130
- artifact_count: 1,
131
- run_count: 1,
132
- },
133
- {
134
- correlation_id: 'newest',
135
- first_seen: now - 10000,
136
- artifact_count: 2,
137
- run_count: 1,
138
- },
139
- {
140
- correlation_id: 'middle',
141
- first_seen: now - 120000,
142
- artifact_count: 3,
143
- run_count: 1,
144
- },
145
- ];
146
-
147
- store.updateAvailableCorrelationIds(metadata);
148
-
83
+ it('should update available facets', () => {
84
+ const facets: FilterFacets = {
85
+ artifactTypes: ['TypeB', 'TypeA'],
86
+ producers: ['b', 'a'],
87
+ tags: ['beta', 'alpha'],
88
+ visibilities: ['Private', 'Public'],
89
+ };
90
+ useFilterStore.getState().updateAvailableFacets(facets);
149
91
  const state = useFilterStore.getState();
150
- expect(state.availableCorrelationIds[0]?.correlation_id).toBe('newest');
151
- expect(state.availableCorrelationIds[1]?.correlation_id).toBe('middle');
152
- expect(state.availableCorrelationIds[2]?.correlation_id).toBe('oldest');
92
+ expect(state.availableArtifactTypes).toEqual(['TypeA', 'TypeB']);
93
+ expect(state.availableProducers).toEqual(['a', 'b']);
94
+ expect(state.availableTags).toEqual(['alpha', 'beta']);
95
+ expect(state.availableVisibility).toEqual(['Private', 'Public']);
153
96
  });
154
97
  });
155
98
 
156
- describe('getActiveFilters', () => {
157
- it('should return empty array when no filters active', () => {
158
- const store = useFilterStore.getState();
159
- const activeFilters = store.getActiveFilters();
160
- expect(activeFilters).toEqual([]);
161
- });
162
-
163
- it('should return correlation ID filter when set', () => {
164
- const store = useFilterStore.getState();
165
- store.setCorrelationId('test-123');
166
-
167
- const activeFilters = store.getActiveFilters();
168
- expect(activeFilters).toHaveLength(1);
169
- expect(activeFilters[0]).toEqual({
170
- type: 'correlationId',
171
- value: 'test-123',
172
- label: 'Correlation ID: test-123',
173
- });
174
- });
175
-
176
- it('should return time range filter when not default', () => {
99
+ describe('active filters', () => {
100
+ it('should return active filters across selections', () => {
177
101
  const store = useFilterStore.getState();
102
+ store.setCorrelationId('abc');
178
103
  store.setTimeRange({ preset: 'last5min' });
104
+ store.setArtifactTypes(['TypeA']);
105
+ store.setProducers(['agent1']);
106
+ store.setTags(['urgent']);
107
+ store.setVisibility(['Public']);
179
108
 
180
- const activeFilters = store.getActiveFilters();
181
- expect(activeFilters).toHaveLength(1);
182
- expect(activeFilters[0]).toEqual({
183
- type: 'timeRange',
184
- value: { preset: 'last5min' },
185
- label: 'Time: Last 5 min',
186
- });
109
+ const active = store.getActiveFilters();
110
+ expect(active).toHaveLength(6);
111
+ expect(active.map((f) => f.type)).toContain('visibility');
187
112
  });
188
113
 
189
- it('should return custom time range filter with formatted dates', () => {
114
+ it('should remove individual filters', () => {
190
115
  const store = useFilterStore.getState();
191
- const start = Date.now() - 3600000;
192
- const end = Date.now();
193
- store.setTimeRange({ preset: 'custom', start, end });
194
-
195
- const activeFilters = store.getActiveFilters();
196
- expect(activeFilters).toHaveLength(1);
197
- expect(activeFilters[0]?.type).toBe('timeRange');
198
- expect(activeFilters[0]?.label).toMatch(/^Time: /);
199
- });
116
+ store.setArtifactTypes(['TypeA', 'TypeB']);
200
117
 
201
- it('should return both filters when both are active', () => {
202
- const store = useFilterStore.getState();
203
- store.setCorrelationId('test-123');
204
- store.setTimeRange({ preset: 'last1hour' });
118
+ const [firstFilter] = store.getActiveFilters().filter((f) => f.type === 'artifactTypes');
119
+ expect(firstFilter?.value).toBe('TypeA');
205
120
 
206
- const activeFilters = store.getActiveFilters();
207
- expect(activeFilters).toHaveLength(2);
121
+ useFilterStore.getState().removeFilter(firstFilter!);
122
+ expect(useFilterStore.getState().selectedArtifactTypes).toEqual(['TypeB']);
208
123
  });
209
124
  });
210
125
 
211
- describe('removeFilter', () => {
212
- it('should remove correlation ID filter', () => {
126
+ describe('snapshots', () => {
127
+ it('should export and reapply filter snapshots', () => {
213
128
  const store = useFilterStore.getState();
214
- store.setCorrelationId('test-123');
129
+ store.setCorrelationId('snapshot');
130
+ store.setArtifactTypes(['TypeA']);
215
131
 
216
- store.removeFilter('correlationId');
217
-
218
- expect(useFilterStore.getState().correlationId).toBeNull();
219
- });
220
-
221
- it('should reset time range filter to default', () => {
222
- const store = useFilterStore.getState();
223
- store.setTimeRange({ preset: 'last1hour' });
224
-
225
- store.removeFilter('timeRange');
226
-
227
- expect(useFilterStore.getState().timeRange).toEqual({ preset: 'last10min' });
228
- });
229
-
230
- it('should not affect other filters', () => {
231
- const store = useFilterStore.getState();
232
- store.setCorrelationId('test-123');
233
- store.setTimeRange({ preset: 'last1hour' });
234
-
235
- store.removeFilter('correlationId');
132
+ const snapshot = store.getFilterSnapshot();
133
+ useFilterStore.getState().clearFilters();
134
+ useFilterStore.getState().applyFilterSnapshot(snapshot);
236
135
 
237
136
  const state = useFilterStore.getState();
238
- expect(state.correlationId).toBeNull();
239
- expect(state.timeRange).toEqual({ preset: 'last1hour' });
137
+ expect(state.correlationId).toBe('snapshot');
138
+ expect(state.selectedArtifactTypes).toEqual(['TypeA']);
139
+ });
140
+
141
+ it('should manage saved filters list', () => {
142
+ const snapshot: FilterSnapshot = {
143
+ correlationId: null,
144
+ timeRange: { preset: 'last10min' },
145
+ artifactTypes: [],
146
+ producers: [],
147
+ tags: [],
148
+ visibility: [],
149
+ };
150
+ useFilterStore.getState().addSavedFilter({
151
+ filter_id: '1',
152
+ name: 'Default',
153
+ created_at: Date.now(),
154
+ filters: snapshot,
155
+ });
156
+
157
+ expect(useFilterStore.getState().savedFilters).toHaveLength(1);
158
+ useFilterStore.getState().removeSavedFilter('1');
159
+ expect(useFilterStore.getState().savedFilters).toHaveLength(0);
240
160
  });
241
161
  });
242
162
  });
@@ -1,72 +1,169 @@
1
1
  import { create } from 'zustand';
2
2
  import { devtools } from 'zustand/middleware';
3
- import { TimeRange, CorrelationIdMetadata } from '../types/filters';
3
+ import {
4
+ TimeRange,
5
+ CorrelationIdMetadata,
6
+ FilterFacets,
7
+ ArtifactSummary,
8
+ SavedFilterMeta,
9
+ FilterSnapshot,
10
+ } from '../types/filters';
11
+
12
+ export type ActiveFilterType =
13
+ | 'correlationId'
14
+ | 'timeRange'
15
+ | 'artifactTypes'
16
+ | 'producers'
17
+ | 'tags'
18
+ | 'visibility';
4
19
 
5
20
  export interface ActiveFilter {
6
- type: 'correlationId' | 'timeRange';
21
+ type: ActiveFilterType;
7
22
  value: string | TimeRange;
8
23
  label: string;
9
24
  }
10
25
 
11
26
  interface FilterState {
12
- // Active filters
13
27
  correlationId: string | null;
14
28
  timeRange: TimeRange;
15
-
16
- // Autocomplete data
29
+ selectedArtifactTypes: string[];
30
+ selectedProducers: string[];
31
+ selectedTags: string[];
32
+ selectedVisibility: string[];
17
33
  availableCorrelationIds: CorrelationIdMetadata[];
34
+ availableArtifactTypes: string[];
35
+ availableProducers: string[];
36
+ availableTags: string[];
37
+ availableVisibility: string[];
38
+ summary: ArtifactSummary | null;
39
+ savedFilters: SavedFilterMeta[];
18
40
 
19
- // Actions
20
41
  setCorrelationId: (id: string | null) => void;
21
42
  setTimeRange: (range: TimeRange) => void;
43
+ setArtifactTypes: (types: string[]) => void;
44
+ setProducers: (producers: string[]) => void;
45
+ setTags: (tags: string[]) => void;
46
+ setVisibility: (visibility: string[]) => void;
22
47
  clearFilters: () => void;
23
48
 
24
- // Update available IDs
25
49
  updateAvailableCorrelationIds: (metadata: CorrelationIdMetadata[]) => void;
50
+ updateAvailableFacets: (facets: FilterFacets) => void;
51
+ setSummary: (summary: ArtifactSummary | null) => void;
26
52
 
27
- // Get active filters
28
- getActiveFilters: () => ActiveFilter[];
53
+ setSavedFilters: (filters: SavedFilterMeta[]) => void;
54
+ addSavedFilter: (filter: SavedFilterMeta) => void;
55
+ removeSavedFilter: (filterId: string) => void;
29
56
 
30
- // Remove specific filter
31
- removeFilter: (type: 'correlationId' | 'timeRange') => void;
57
+ getActiveFilters: () => ActiveFilter[];
58
+ removeFilter: (filter: ActiveFilter) => void;
59
+ getFilterSnapshot: () => FilterSnapshot;
60
+ applyFilterSnapshot: (snapshot: FilterSnapshot) => void;
32
61
  }
33
62
 
34
- const formatTimeRange = (range: TimeRange): string => {
63
+ const defaultTimeRange: TimeRange = { preset: 'last10min' };
64
+
65
+ export const formatTimeRange = (range: TimeRange): string => {
35
66
  if (range.preset === 'last5min') return 'Last 5 min';
36
67
  if (range.preset === 'last10min') return 'Last 10 min';
37
68
  if (range.preset === 'last1hour') return 'Last hour';
69
+ if (range.preset === 'all') return 'All time';
38
70
  if (range.preset === 'custom' && range.start && range.end) {
39
71
  const startDate = new Date(range.start).toLocaleString();
40
72
  const endDate = new Date(range.end).toLocaleString();
41
73
  return `${startDate} - ${endDate}`;
42
74
  }
43
- return 'Unknown';
75
+ if (range.preset === 'custom') {
76
+ return 'Custom range';
77
+ }
78
+ return 'Last 10 min';
44
79
  };
45
80
 
81
+ const uniqueSorted = (items: string[]) => Array.from(new Set(items)).sort((a, b) => a.localeCompare(b));
82
+
46
83
  export const useFilterStore = create<FilterState>()(
47
84
  devtools(
48
85
  (set, get) => ({
49
86
  correlationId: null,
50
- timeRange: { preset: 'last10min' },
87
+ timeRange: defaultTimeRange,
88
+ selectedArtifactTypes: [],
89
+ selectedProducers: [],
90
+ selectedTags: [],
91
+ selectedVisibility: [],
51
92
  availableCorrelationIds: [],
93
+ availableArtifactTypes: [],
94
+ availableProducers: [],
95
+ availableTags: [],
96
+ availableVisibility: [],
97
+ summary: null,
98
+ savedFilters: [],
52
99
 
53
100
  setCorrelationId: (id) => set({ correlationId: id }),
54
101
  setTimeRange: (range) => set({ timeRange: range }),
102
+ setArtifactTypes: (types) => set({ selectedArtifactTypes: uniqueSorted(types) }),
103
+ setProducers: (producers) => set({ selectedProducers: uniqueSorted(producers) }),
104
+ setTags: (tags) => set({ selectedTags: uniqueSorted(tags) }),
105
+ setVisibility: (visibility) => set({ selectedVisibility: uniqueSorted(visibility) }),
55
106
  clearFilters: () =>
56
107
  set({
57
108
  correlationId: null,
58
- timeRange: { preset: 'last10min' },
109
+ timeRange: defaultTimeRange,
110
+ selectedArtifactTypes: [],
111
+ selectedProducers: [],
112
+ selectedTags: [],
113
+ selectedVisibility: [],
59
114
  }),
60
115
 
61
116
  updateAvailableCorrelationIds: (metadata) => {
62
- // Sort by most recent first
63
117
  const sorted = [...metadata].sort((a, b) => b.first_seen - a.first_seen);
64
118
  set({
65
119
  availableCorrelationIds: sorted.slice(0, 50),
66
120
  });
67
121
  },
68
122
 
123
+ updateAvailableFacets: (facets) =>
124
+ set({
125
+ availableArtifactTypes: uniqueSorted(facets.artifactTypes),
126
+ availableProducers: uniqueSorted(facets.producers),
127
+ availableTags: uniqueSorted(facets.tags),
128
+ availableVisibility: uniqueSorted(facets.visibilities),
129
+ }),
130
+
131
+ setSummary: (summary) => set({ summary }),
132
+
133
+ setSavedFilters: (filters) => set({ savedFilters: filters }),
134
+ addSavedFilter: (filter) =>
135
+ set((state) => ({
136
+ savedFilters: [...state.savedFilters.filter((f) => f.filter_id !== filter.filter_id), filter],
137
+ })),
138
+ removeSavedFilter: (filterId) =>
139
+ set((state) => ({
140
+ savedFilters: state.savedFilters.filter((f) => f.filter_id !== filterId),
141
+ })),
142
+
143
+ getFilterSnapshot: () => {
144
+ const state = get();
145
+ return {
146
+ correlationId: state.correlationId,
147
+ timeRange: state.timeRange,
148
+ artifactTypes: [...state.selectedArtifactTypes],
149
+ producers: [...state.selectedProducers],
150
+ tags: [...state.selectedTags],
151
+ visibility: [...state.selectedVisibility],
152
+ };
153
+ },
154
+
155
+ applyFilterSnapshot: (snapshot) =>
156
+ set({
157
+ correlationId: snapshot.correlationId,
158
+ timeRange: snapshot.timeRange,
159
+ selectedArtifactTypes: uniqueSorted(snapshot.artifactTypes),
160
+ selectedProducers: uniqueSorted(snapshot.producers),
161
+ selectedTags: uniqueSorted(snapshot.tags),
162
+ selectedVisibility: uniqueSorted(snapshot.visibility),
163
+ }),
164
+
69
165
  getActiveFilters: () => {
166
+ console.debug('[filterStore] computing active filters', { correlationId: get().correlationId, timeRange: get().timeRange });
70
167
  const state = get();
71
168
  const filters: ActiveFilter[] = [];
72
169
 
@@ -78,24 +175,74 @@ export const useFilterStore = create<FilterState>()(
78
175
  });
79
176
  }
80
177
 
81
- // Only add time range if it's not the default
82
- if (state.timeRange.preset !== 'last10min') {
178
+ filters.push({
179
+ type: 'timeRange',
180
+ value: state.timeRange,
181
+ label: `Time: ${formatTimeRange(state.timeRange)}`,
182
+ });
183
+
184
+ state.selectedArtifactTypes.forEach((type) => {
83
185
  filters.push({
84
- type: 'timeRange',
85
- value: state.timeRange,
86
- label: `Time: ${formatTimeRange(state.timeRange)}`,
186
+ type: 'artifactTypes',
187
+ value: type,
188
+ label: `Type: ${type}`,
87
189
  });
88
- }
190
+ });
191
+
192
+ state.selectedProducers.forEach((producer) => {
193
+ filters.push({
194
+ type: 'producers',
195
+ value: producer,
196
+ label: `Producer: ${producer}`,
197
+ });
198
+ });
199
+
200
+ state.selectedTags.forEach((tag) => {
201
+ filters.push({
202
+ type: 'tags',
203
+ value: tag,
204
+ label: `Tag: ${tag}`,
205
+ });
206
+ });
207
+
208
+ state.selectedVisibility.forEach((kind) => {
209
+ filters.push({
210
+ type: 'visibility',
211
+ value: kind,
212
+ label: `Visibility: ${kind}`,
213
+ });
214
+ });
89
215
 
90
216
  return filters;
91
217
  },
92
218
 
93
- removeFilter: (type) => {
94
- if (type === 'correlationId') {
219
+ removeFilter: (filter) => {
220
+ if (filter.type === 'correlationId') {
95
221
  set({ correlationId: null });
96
- } else if (type === 'timeRange') {
97
- set({ timeRange: { preset: 'last10min' } });
222
+ return;
223
+ }
224
+ if (filter.type === 'timeRange') {
225
+ set({ timeRange: defaultTimeRange });
226
+ return;
98
227
  }
228
+ if (typeof filter.value !== 'string') {
229
+ return;
230
+ }
231
+ set((state) => {
232
+ if (filter.type === 'artifactTypes') {
233
+ return { selectedArtifactTypes: state.selectedArtifactTypes.filter((item) => item !== filter.value) };
234
+ }
235
+ if (filter.type === 'producers') {
236
+ return { selectedProducers: state.selectedProducers.filter((item) => item !== filter.value) };
237
+ }
238
+ if (filter.type === 'tags') {
239
+ return { selectedTags: state.selectedTags.filter((item) => item !== filter.value) };
240
+ }
241
+ if (filter.type === 'visibility') {
242
+ return { selectedVisibility: state.selectedVisibility.filter((item) => item !== filter.value) };
243
+ }
244
+ return {};
245
+ });
99
246
  },
100
247
  }),
101
248
  { name: 'filterStore' }
@@ -9,6 +9,8 @@ describe('graphStore', () => {
9
9
  agents: new Map(),
10
10
  messages: new Map(),
11
11
  events: [],
12
+ runs: new Map(),
13
+ consumptions: new Map(),
12
14
  nodes: [],
13
15
  edges: [],
14
16
  });
@@ -183,4 +185,21 @@ describe('graphStore', () => {
183
185
  expect(nodes[0]?.type).toBe('agent');
184
186
  expect(edges.length).toBeGreaterThan(0);
185
187
  });
188
+
189
+ it('should hydrate consumptions from message payload', () => {
190
+ const message: Message = {
191
+ id: 'msg-embed',
192
+ type: 'Recipe',
193
+ payload: {},
194
+ timestamp: Date.now(),
195
+ correlationId: 'corr-embed',
196
+ producedBy: 'chef',
197
+ consumedBy: ['critic'],
198
+ };
199
+
200
+ useGraphStore.getState().batchUpdate({ messages: [message] });
201
+
202
+ const state = useGraphStore.getState();
203
+ expect(state.consumptions.get('msg-embed')).toEqual(['critic']);
204
+ });
186
205
  });