flock-core 0.5.0b65__py3-none-any.whl → 0.5.0b71__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 +41 -5
  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.0b71.dist-info}/METADATA +26 -1
  47. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b71.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.0b71.dist-info}/WHEEL +0 -0
  55. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b71.dist-info}/entry_points.txt +0 -0
  56. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b71.dist-info}/licenses/LICENSE +0 -0
flock/cli.py CHANGED
@@ -3,14 +3,17 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ from datetime import datetime
6
7
 
7
8
  import typer
8
9
  from rich.console import Console
9
10
  from rich.table import Table
11
+ from typer.models import OptionInfo
10
12
 
11
13
  # Lazy import: only import examples when CLI commands are invoked
12
14
  # This prevents polluting type_registry on every package import
13
15
  from flock.service import BlackboardHTTPService
16
+ from flock.store import SQLiteBlackboardStore
14
17
 
15
18
 
16
19
  app = typer.Typer(help="Blackboard Agents CLI")
@@ -58,16 +61,85 @@ def list_agents() -> None:
58
61
 
59
62
 
60
63
  @app.command()
61
- def serve(host: str = "127.0.0.1", port: int = 8000) -> None:
64
+ def serve(
65
+ host: str = "127.0.0.1",
66
+ port: int = 8000,
67
+ sqlite_db: str | None = typer.Option(None, help="Path to SQLite blackboard store"),
68
+ ) -> None:
62
69
  """Run the HTTP control plane bound to the demo orchestrator."""
63
70
 
64
71
  from flock.examples import create_demo_orchestrator
65
72
 
66
- orchestrator, _ = create_demo_orchestrator()
73
+ if isinstance(sqlite_db, OptionInfo): # Allow direct invocation in tests
74
+ sqlite_db = sqlite_db.default
75
+
76
+ store = None
77
+ if sqlite_db is not None:
78
+ sqlite_store = SQLiteBlackboardStore(sqlite_db)
79
+
80
+ async def _prepare() -> SQLiteBlackboardStore:
81
+ await sqlite_store.ensure_schema()
82
+ return sqlite_store
83
+
84
+ store = asyncio.run(_prepare())
85
+
86
+ orchestrator, _ = create_demo_orchestrator(store=store)
67
87
  service = BlackboardHTTPService(orchestrator)
68
88
  service.run(host=host, port=port)
69
89
 
70
90
 
91
+ @app.command("init-sqlite-store")
92
+ def init_sqlite_store(
93
+ db_path: str = typer.Argument(..., help="Path to SQLite blackboard database"),
94
+ ) -> None:
95
+ """Initialise the SQLite store schema."""
96
+
97
+ store = SQLiteBlackboardStore(db_path)
98
+
99
+ async def _init() -> None:
100
+ await store.ensure_schema()
101
+ await store.close()
102
+
103
+ asyncio.run(_init())
104
+ console.print(f"[green]Initialised SQLite blackboard at {db_path}[/green]")
105
+
106
+
107
+ @app.command("sqlite-maintenance")
108
+ def sqlite_maintenance(
109
+ db_path: str = typer.Argument(..., help="Path to SQLite blackboard database"),
110
+ delete_before: str | None = typer.Option(
111
+ None, help="ISO timestamp; delete artifacts before this time"
112
+ ),
113
+ vacuum: bool = typer.Option(False, help="Run VACUUM after maintenance"),
114
+ ) -> None:
115
+ """Perform maintenance tasks for the SQLite store."""
116
+
117
+ store = SQLiteBlackboardStore(db_path)
118
+
119
+ async def _maintain() -> tuple[int, bool]:
120
+ await store.ensure_schema()
121
+ deleted = 0
122
+ if delete_before is not None:
123
+ try:
124
+ before_dt = datetime.fromisoformat(delete_before)
125
+ except ValueError as exc: # pragma: no cover - Typer handles but defensive
126
+ raise typer.BadParameter(f"Invalid ISO timestamp: {delete_before}") from exc
127
+ deleted = await store.delete_before(before_dt)
128
+ if vacuum:
129
+ await store.vacuum()
130
+ await store.close()
131
+ return deleted, vacuum
132
+
133
+ deleted, vacuum_run = asyncio.run(_maintain())
134
+ console.print(
135
+ f"[yellow]Deleted {deleted} artifacts[/yellow]"
136
+ if delete_before is not None
137
+ else "[yellow]No deletions requested[/yellow]"
138
+ )
139
+ if vacuum_run:
140
+ console.print("[yellow]VACUUM completed[/yellow]")
141
+
142
+
71
143
  def main() -> None:
72
144
  app()
73
145
 
@@ -396,10 +396,46 @@ class DSPyEngine(EngineComponent):
396
396
  if isinstance(raw, BaseModel):
397
397
  return raw.model_dump()
398
398
  if isinstance(raw, str):
399
- try:
400
- return json.loads(raw)
401
- except json.JSONDecodeError:
402
- return {"text": raw}
399
+ text = raw.strip()
400
+ candidates: list[str] = []
401
+
402
+ # Primary attempt - full string
403
+ if text:
404
+ candidates.append(text)
405
+
406
+ # Handle DSPy streaming markers like `[[ ## output ## ]]`
407
+ if text.startswith("[[") and "]]" in text:
408
+ _, remainder = text.split("]]", 1)
409
+ remainder = remainder.strip()
410
+ if remainder:
411
+ candidates.append(remainder)
412
+
413
+ # Handle Markdown-style fenced blocks
414
+ if text.startswith("```") and text.endswith("```"):
415
+ fenced = text.strip("`").strip()
416
+ if fenced:
417
+ candidates.append(fenced)
418
+
419
+ # Extract first JSON-looking segment if present
420
+ for opener, closer in (("{", "}"), ("[", "]")):
421
+ start = text.find(opener)
422
+ end = text.rfind(closer)
423
+ if start != -1 and end != -1 and end > start:
424
+ segment = text[start : end + 1].strip()
425
+ if segment:
426
+ candidates.append(segment)
427
+
428
+ seen: set[str] = set()
429
+ for candidate in candidates:
430
+ if candidate in seen:
431
+ continue
432
+ seen.add(candidate)
433
+ try:
434
+ return json.loads(candidate)
435
+ except json.JSONDecodeError:
436
+ continue
437
+
438
+ return {"text": text}
403
439
  if isinstance(raw, Mapping):
404
440
  return dict(raw)
405
441
  return {"value": raw}
@@ -633,7 +669,7 @@ class DSPyEngine(EngineComponent):
633
669
  except Exception as e:
634
670
  logger.warning(f"Failed to emit streaming event: {e}")
635
671
  else:
636
- logger.exception("NO WS_MANAGER PRESENT!!!!")
672
+ logger.debug("No WebSocket manager present for streaming event.")
637
673
 
638
674
  if formatter is not None:
639
675
  _refresh_panel()
flock/examples.py CHANGED
@@ -18,6 +18,7 @@ from flock.components import EngineComponent
18
18
  from flock.orchestrator import Flock
19
19
  from flock.registry import flock_tool, flock_type, type_registry
20
20
  from flock.runtime import EvalInputs, EvalResult
21
+ from flock.store import BlackboardStore
21
22
  from flock.utilities import LoggingUtility, MetricsUtility
22
23
 
23
24
 
@@ -75,8 +76,10 @@ class TaglineEngine(EngineComponent):
75
76
 
76
77
  def create_demo_orchestrator(
77
78
  model: str | None = None,
79
+ *,
80
+ store: BlackboardStore | None = None,
78
81
  ) -> tuple[Flock, dict[str, AgentBuilder]]:
79
- orchestrator = Flock(model)
82
+ orchestrator = Flock(model, store=store)
80
83
 
81
84
  movie = (
82
85
  orchestrator.agent("movie")
flock/frontend/README.md CHANGED
@@ -33,7 +33,7 @@ The dashboard offers two complementary visualization modes:
33
33
 
34
34
  ### Extensible Module System
35
35
  - **Custom Visualizations**: Add specialized views via the module system
36
- - **Event Log Module**: Built-in table view for detailed event inspection
36
+ - **Historical Blackboard Module**: Persisted artifact browser with retention insights
37
37
  - **Trace Viewer Module**: Jaeger-style distributed tracing with timeline and statistics
38
38
  - **Context Menu Integration**: Right-click to add modules at any location
39
39
  - **Persistent Layout**: Module positions and sizes are saved across sessions
@@ -123,6 +123,20 @@ Every traced operation captures:
123
123
  - **Multi-Trace Comparison**: Open related traces to compare execution patterns
124
124
  - **JSON Navigation**: Use "Expand All" for complex nested structures
125
125
 
126
+ ### Historical Blackboard Module 📚
127
+
128
+ The new Historical Blackboard module brings persisted artifacts into the dashboard so operators can rewind the blackboard, not just watch the live firehose.
129
+
130
+ #### Highlights
131
+
132
+ - **SQLite-first loading**: Fetches paginated artifacts before WebSocket replay, so the graph and detail views start with real history.
133
+ - **Rich filtering**: Mirrors server-side `FilterConfig` capabilities (type, producer, tags, visibility, correlation, time range) with multi-select controls and saved presets.
134
+ - **Consumption awareness**: Displays who consumed each artifact, run IDs, and consumption timestamps—ideal for reconciling downstream behaviour.
135
+ - **Retention transparency**: Inline banners show the oldest/latest artifacts on disk and whether additional data can be loaded.
136
+ - **Virtualized table**: Efficiently scroll through thousands of artifacts with keyboard navigation, quick selection, and payload inspection via the JSON renderer.
137
+
138
+ Launch the module via the context menu (or `Add Module → Historical Blackboard`) after running `examples/03-the-dashboard/04_persistent_pizza_dashboard.py` against a SQLite-backed orchestrator.
139
+
126
140
  ### Modern UI/UX
127
141
  - **Glassmorphism Design**: Modern dark theme with semi-transparent surfaces and blur effects
128
142
  - **Keyboard Shortcuts**: Navigate efficiently with Ctrl+M, Ctrl+F, and Esc
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "flock-ui",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "flock-ui",
9
- "version": "0.1.4",
9
+ "version": "0.1.7",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@types/dagre": "^0.7.53",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flock-ui",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "Flock Flow Real-Time Dashboard Frontend",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -5,9 +5,12 @@ import { measureRenderTime } from './utils/performance';
5
5
  import { initializeWebSocket } from './services/websocket';
6
6
  import { registerModules } from './components/modules/registerModules';
7
7
  import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
8
- import { fetchRegisteredAgents } from './services/api';
8
+ import { fetchRegisteredAgents, fetchArtifactSummary, fetchArtifacts } from './services/api';
9
9
  import { useGraphStore } from './store/graphStore';
10
10
  import { useUIStore } from './store/uiStore';
11
+ import { useFilterStore } from './store/filterStore';
12
+ import { mapArtifactToMessage } from './utils/artifacts';
13
+ import { indexedDBService } from './services/indexeddb';
11
14
 
12
15
  // Register modules once at module load time
13
16
  registerModules();
@@ -30,10 +33,59 @@ const App: React.FC = () => {
30
33
  }
31
34
  });
32
35
 
33
- // Initialize WebSocket connection
34
- const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws';
35
- const wsClient = initializeWebSocket(wsUrl);
36
- wsClient.connect();
36
+ const loadHistoricalData = async () => {
37
+ try {
38
+ await indexedDBService.initialize();
39
+
40
+ const filterStore = useFilterStore.getState();
41
+ const graphStore = useGraphStore.getState();
42
+ const uiStore = useUIStore.getState();
43
+
44
+ const summary = await fetchArtifactSummary();
45
+ filterStore.setSummary(summary);
46
+ filterStore.updateAvailableFacets({
47
+ artifactTypes: Object.keys(summary.by_type),
48
+ producers: Object.keys(summary.by_producer),
49
+ tags: Object.keys(summary.tag_counts),
50
+ visibilities: Object.keys(summary.by_visibility),
51
+ });
52
+
53
+ const artifactResponse = await fetchArtifacts({ limit: 200, embedMeta: true });
54
+ const messages = artifactResponse.items.map(mapArtifactToMessage);
55
+ if (messages.length > 0) {
56
+ graphStore.batchUpdate({ messages });
57
+ if (uiStore.mode === 'blackboard') {
58
+ graphStore.generateBlackboardViewGraph();
59
+ } else {
60
+ graphStore.generateAgentViewGraph();
61
+ }
62
+ graphStore.applyFilters();
63
+
64
+ const correlationMetadata = new Map<string, { correlation_id: string; first_seen: number; artifact_count: number; run_count: number }>();
65
+ artifactResponse.items.forEach((item) => {
66
+ if (!item.correlation_id) return;
67
+ const timestamp = new Date(item.created_at).getTime();
68
+ const existing = correlationMetadata.get(item.correlation_id);
69
+ if (existing) {
70
+ existing.artifact_count += 1;
71
+ existing.first_seen = Math.min(existing.first_seen, timestamp);
72
+ } else {
73
+ correlationMetadata.set(item.correlation_id, {
74
+ correlation_id: item.correlation_id,
75
+ first_seen: timestamp,
76
+ artifact_count: 1,
77
+ run_count: 0,
78
+ });
79
+ }
80
+ });
81
+ if (correlationMetadata.size > 0) {
82
+ filterStore.updateAvailableCorrelationIds(Array.from(correlationMetadata.values()));
83
+ }
84
+ }
85
+ } catch (error) {
86
+ console.error('[App] Failed to load historical artifacts:', error);
87
+ }
88
+ };
37
89
 
38
90
  // Load registered agents from orchestrator
39
91
  // This pre-populates the graph with all agent nodes before any events occur
@@ -61,10 +113,26 @@ const App: React.FC = () => {
61
113
  }
62
114
  };
63
115
 
64
- loadInitialAgents();
116
+ const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws';
117
+ const wsClient = initializeWebSocket(wsUrl);
118
+ let cancelled = false;
119
+
120
+ const bootstrap = async () => {
121
+ await loadHistoricalData();
122
+ await loadInitialAgents();
123
+
124
+ if (!cancelled) {
125
+ wsClient.connect();
126
+ }
127
+ };
128
+
129
+ bootstrap().catch((error) => {
130
+ console.error('[App] Bootstrap failed:', error);
131
+ });
65
132
 
66
133
  // Cleanup on unmount
67
134
  return () => {
135
+ cancelled = true;
68
136
  wsClient.disconnect();
69
137
  };
70
138
  }, []);
@@ -404,18 +404,17 @@ describe('Critical E2E Scenarios (Frontend)', () => {
404
404
  artifact_count: 1,
405
405
  run_count: 1,
406
406
  }));
407
+ // Reset filters and generate graph with all events (use fresh state)
408
+ useFilterStore.getState().clearFilters();
407
409
  useFilterStore.getState().updateAvailableCorrelationIds(metadata);
408
410
 
409
- // Generate graph with all events (use fresh state)
410
411
  await act(async () => {
411
412
  useGraphStore.getState().generateBlackboardViewGraph();
412
413
  });
413
414
 
414
- // Wait for nodes to be generated
415
+ // Ensure messages were recorded
415
416
  await waitFor(() => {
416
- const nodes = useGraphStore.getState().nodes;
417
- const visibleNodes = nodes.filter((n) => !n.hidden);
418
- expect(visibleNodes.length).toBe(3);
417
+ expect(useGraphStore.getState().messages.size).toBe(3);
419
418
  }, { timeout: 5000 });
420
419
 
421
420
  // Apply correlation ID filter (use fresh state)
@@ -329,9 +329,13 @@ describe('Filtering Integration E2E', () => {
329
329
 
330
330
  const state = useGraphStore.getState();
331
331
  const visibleNodes = state.nodes.filter((n) => !n.hidden);
332
- // Only agent-1 should be visible
333
- expect(visibleNodes).toHaveLength(1);
334
- expect(visibleNodes[0]?.id).toBe('agent-1');
332
+
333
+ // Agent nodes remain visible but metrics reflect filtered artifacts
334
+ expect(visibleNodes).toHaveLength(2);
335
+ const agent1Node = visibleNodes.find((n) => n.id === 'agent-1');
336
+ const agent2Node = visibleNodes.find((n) => n.id === 'agent-2');
337
+ expect(agent1Node).toBeDefined();
338
+ expect(agent2Node).toBeDefined();
335
339
  });
336
340
  });
337
341
 
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import MultiSelect from '../settings/MultiSelect';
3
+ import { useFilterStore } from '../../store/filterStore';
4
+
5
+ const ArtifactTypeFilter: React.FC = () => {
6
+ const options = useFilterStore((state) => state.availableArtifactTypes);
7
+ const selected = useFilterStore((state) => state.selectedArtifactTypes);
8
+ const setArtifactTypes = useFilterStore((state) => state.setArtifactTypes);
9
+
10
+ return (
11
+ <MultiSelect
12
+ options={options}
13
+ selected={selected}
14
+ onChange={setArtifactTypes}
15
+ placeholder={options.length ? 'Select types…' : 'No types available'}
16
+ disabled={options.length === 0}
17
+ />
18
+ );
19
+ };
20
+
21
+ export default ArtifactTypeFilter;
@@ -0,0 +1,104 @@
1
+ .panel {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ bottom: 0;
6
+ width: 420px;
7
+ background: var(--color-glass-bg);
8
+ backdrop-filter: blur(var(--blur-lg));
9
+ border-right: var(--border-default);
10
+ box-shadow: var(--shadow-2xl);
11
+ z-index: 1100;
12
+ animation: slideInLeft var(--duration-slow) var(--ease-smooth);
13
+ display: flex;
14
+ flex-direction: column;
15
+ }
16
+
17
+ .header {
18
+ display: flex;
19
+ align-items: flex-start;
20
+ justify-content: space-between;
21
+ gap: var(--space-component-sm);
22
+ padding: var(--space-layout-md);
23
+ border-bottom: var(--border-subtle);
24
+ }
25
+
26
+ .title {
27
+ margin: 0;
28
+ font-size: var(--font-size-h3);
29
+ color: var(--color-text-primary);
30
+ }
31
+
32
+ .subtitle {
33
+ margin: 0;
34
+ font-size: var(--font-size-body-xs);
35
+ color: var(--color-text-tertiary);
36
+ }
37
+
38
+ .closeButton {
39
+ width: 32px;
40
+ height: 32px;
41
+ border-radius: var(--radius-md);
42
+ border: none;
43
+ background: transparent;
44
+ color: var(--color-text-secondary);
45
+ font-size: 22px;
46
+ cursor: pointer;
47
+ transition: var(--transition-all);
48
+ }
49
+
50
+ .closeButton:hover {
51
+ background: var(--color-bg-overlay);
52
+ color: var(--color-text-primary);
53
+ }
54
+
55
+ .content {
56
+ flex: 1;
57
+ overflow-y: auto;
58
+ padding: var(--space-layout-md);
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: var(--space-layout-md);
62
+ }
63
+
64
+ .section {
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: var(--space-component-sm);
68
+ }
69
+
70
+ .sectionLabel {
71
+ margin: 0;
72
+ font-size: var(--font-size-body-xs);
73
+ letter-spacing: var(--letter-spacing-wide);
74
+ color: var(--color-text-tertiary);
75
+ text-transform: uppercase;
76
+ }
77
+
78
+ .separator {
79
+ width: 100%;
80
+ height: 1px;
81
+ background: linear-gradient(90deg, transparent, rgba(148, 163, 184, 0.25), transparent);
82
+ margin: var(--space-component-md) 0;
83
+ }
84
+
85
+ @keyframes slideInLeft {
86
+ from {
87
+ transform: translateX(-40px);
88
+ opacity: 0;
89
+ }
90
+ to {
91
+ transform: translateX(0);
92
+ opacity: 1;
93
+ }
94
+ }
95
+
96
+ @media (max-width: 960px) {
97
+ .panel {
98
+ width: 100%;
99
+ }
100
+
101
+ .panel {
102
+ width: 100%;
103
+ }
104
+ }
@@ -0,0 +1,80 @@
1
+ import React from 'react';
2
+ import CorrelationIDFilter from './CorrelationIDFilter';
3
+ import TimeRangeFilter from './TimeRangeFilter';
4
+ import ArtifactTypeFilter from './ArtifactTypeFilter';
5
+ import ProducerFilter from './ProducerFilter';
6
+ import TagFilter from './TagFilter';
7
+ import VisibilityFilter from './VisibilityFilter';
8
+ import SavedFiltersControl from './SavedFiltersControl';
9
+ import styles from './FilterFlyout.module.css';
10
+
11
+ interface FilterFlyoutProps {
12
+ onClose: () => void;
13
+ }
14
+
15
+ const FilterFlyout: React.FC<FilterFlyoutProps> = ({ onClose }) => {
16
+ return (
17
+ <aside className={styles.panel} role="dialog" aria-label="Filters">
18
+ <header className={styles.header}>
19
+ <div>
20
+ <h2 className={styles.title}>Filters</h2>
21
+ <p className={styles.subtitle}>Slice historical data without losing your place.</p>
22
+ </div>
23
+ <button type="button" className={styles.closeButton} onClick={onClose} aria-label="Close filters">
24
+ ×
25
+ </button>
26
+ </header>
27
+
28
+ <div className={styles.content}>
29
+ <section className={styles.section}>
30
+ <h3 className={styles.sectionLabel}>Presets</h3>
31
+ <SavedFiltersControl />
32
+ </section>
33
+
34
+ <div className={styles.separator} role="presentation" />
35
+
36
+ <section className={styles.section}>
37
+ <h3 className={styles.sectionLabel}>Correlation</h3>
38
+ <CorrelationIDFilter />
39
+ </section>
40
+
41
+ <div className={styles.separator} role="presentation" />
42
+
43
+ <section className={styles.section}>
44
+ <h3 className={styles.sectionLabel}>Time Range</h3>
45
+ <TimeRangeFilter />
46
+ </section>
47
+
48
+ <div className={styles.separator} role="presentation" />
49
+
50
+ <section className={styles.section}>
51
+ <h3 className={styles.sectionLabel}>Artifact Types</h3>
52
+ <ArtifactTypeFilter />
53
+ </section>
54
+
55
+ <div className={styles.separator} role="presentation" />
56
+
57
+ <section className={styles.section}>
58
+ <h3 className={styles.sectionLabel}>Producers</h3>
59
+ <ProducerFilter />
60
+ </section>
61
+
62
+ <div className={styles.separator} role="presentation" />
63
+
64
+ <section className={styles.section}>
65
+ <h3 className={styles.sectionLabel}>Tags</h3>
66
+ <TagFilter />
67
+ </section>
68
+
69
+ <div className={styles.separator} role="presentation" />
70
+
71
+ <section className={styles.section}>
72
+ <h3 className={styles.sectionLabel}>Visibility</h3>
73
+ <VisibilityFilter />
74
+ </section>
75
+ </div>
76
+ </aside>
77
+ );
78
+ };
79
+
80
+ export default FilterFlyout;