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
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(
|
|
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
|
-
|
|
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
|
|
flock/engines/dspy_engine.py
CHANGED
|
@@ -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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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}
|
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
|
-
- **
|
|
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
|
flock/frontend/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flock-ui",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
9
|
+
"version": "0.1.7",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@types/dagre": "^0.7.53",
|
flock/frontend/package.json
CHANGED
flock/frontend/src/App.tsx
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
415
|
+
// Ensure messages were recorded
|
|
415
416
|
await waitFor(() => {
|
|
416
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
expect(visibleNodes
|
|
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;
|