flock-core 0.5.0b50__py3-none-any.whl → 0.5.0b52__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/dashboard/launcher.py +1 -1
- flock/frontend/README.md +678 -0
- flock/frontend/docs/DESIGN_SYSTEM.md +1980 -0
- flock/frontend/index.html +12 -0
- flock/frontend/package-lock.json +4347 -0
- flock/frontend/package.json +48 -0
- flock/frontend/src/App.tsx +79 -0
- flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +587 -0
- flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +387 -0
- flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +640 -0
- flock/frontend/src/__tests__/integration/indexeddb-persistence.test.tsx +699 -0
- flock/frontend/src/components/common/BuildInfo.tsx +39 -0
- flock/frontend/src/components/common/EmptyState.module.css +115 -0
- flock/frontend/src/components/common/EmptyState.tsx +128 -0
- flock/frontend/src/components/common/ErrorBoundary.module.css +169 -0
- flock/frontend/src/components/common/ErrorBoundary.tsx +118 -0
- flock/frontend/src/components/common/KeyboardShortcutsDialog.css +251 -0
- flock/frontend/src/components/common/KeyboardShortcutsDialog.tsx +151 -0
- flock/frontend/src/components/common/LoadingSpinner.module.css +97 -0
- flock/frontend/src/components/common/LoadingSpinner.tsx +29 -0
- flock/frontend/src/components/controls/PublishControl.css +547 -0
- flock/frontend/src/components/controls/PublishControl.test.tsx +543 -0
- flock/frontend/src/components/controls/PublishControl.tsx +432 -0
- flock/frontend/src/components/details/DetailWindowContainer.tsx +62 -0
- flock/frontend/src/components/details/LiveOutputTab.test.tsx +792 -0
- flock/frontend/src/components/details/LiveOutputTab.tsx +220 -0
- flock/frontend/src/components/details/MessageHistoryTab.tsx +299 -0
- flock/frontend/src/components/details/NodeDetailWindow.test.tsx +501 -0
- flock/frontend/src/components/details/NodeDetailWindow.tsx +218 -0
- flock/frontend/src/components/details/RunStatusTab.tsx +307 -0
- flock/frontend/src/components/details/tabs.test.tsx +1015 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.module.css +102 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.test.tsx +197 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.tsx +121 -0
- flock/frontend/src/components/filters/FilterBar.module.css +29 -0
- flock/frontend/src/components/filters/FilterBar.test.tsx +133 -0
- flock/frontend/src/components/filters/FilterBar.tsx +33 -0
- flock/frontend/src/components/filters/FilterPills.module.css +79 -0
- flock/frontend/src/components/filters/FilterPills.test.tsx +173 -0
- flock/frontend/src/components/filters/FilterPills.tsx +67 -0
- flock/frontend/src/components/filters/TimeRangeFilter.module.css +91 -0
- flock/frontend/src/components/filters/TimeRangeFilter.test.tsx +154 -0
- flock/frontend/src/components/filters/TimeRangeFilter.tsx +105 -0
- flock/frontend/src/components/graph/AgentNode.test.tsx +75 -0
- flock/frontend/src/components/graph/AgentNode.tsx +322 -0
- flock/frontend/src/components/graph/GraphCanvas.tsx +406 -0
- flock/frontend/src/components/graph/MessageFlowEdge.tsx +128 -0
- flock/frontend/src/components/graph/MessageNode.test.tsx +62 -0
- flock/frontend/src/components/graph/MessageNode.tsx +116 -0
- flock/frontend/src/components/graph/MiniMap.tsx +47 -0
- flock/frontend/src/components/graph/TransformEdge.tsx +123 -0
- flock/frontend/src/components/layout/DashboardLayout.css +407 -0
- flock/frontend/src/components/layout/DashboardLayout.tsx +300 -0
- flock/frontend/src/components/layout/Header.module.css +88 -0
- flock/frontend/src/components/layout/Header.tsx +52 -0
- flock/frontend/src/components/modules/EventLogModule.test.tsx +401 -0
- flock/frontend/src/components/modules/EventLogModule.tsx +396 -0
- flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +17 -0
- flock/frontend/src/components/modules/ModuleRegistry.test.ts +333 -0
- flock/frontend/src/components/modules/ModuleRegistry.ts +85 -0
- flock/frontend/src/components/modules/ModuleWindow.tsx +155 -0
- flock/frontend/src/components/modules/registerModules.ts +20 -0
- flock/frontend/src/components/settings/AdvancedSettings.tsx +175 -0
- flock/frontend/src/components/settings/AppearanceSettings.tsx +185 -0
- flock/frontend/src/components/settings/GraphSettings.tsx +110 -0
- flock/frontend/src/components/settings/SettingsPanel.css +327 -0
- flock/frontend/src/components/settings/SettingsPanel.tsx +131 -0
- flock/frontend/src/components/settings/ThemeSelector.tsx +298 -0
- flock/frontend/src/hooks/useKeyboardShortcuts.ts +148 -0
- flock/frontend/src/hooks/useModulePersistence.test.ts +442 -0
- flock/frontend/src/hooks/useModulePersistence.ts +154 -0
- flock/frontend/src/hooks/useModules.ts +139 -0
- flock/frontend/src/hooks/usePersistence.ts +139 -0
- flock/frontend/src/main.tsx +13 -0
- flock/frontend/src/services/api.ts +213 -0
- flock/frontend/src/services/indexeddb.test.ts +793 -0
- flock/frontend/src/services/indexeddb.ts +794 -0
- flock/frontend/src/services/layout.test.ts +437 -0
- flock/frontend/src/services/layout.ts +146 -0
- flock/frontend/src/services/themeApplicator.ts +140 -0
- flock/frontend/src/services/themeService.ts +77 -0
- flock/frontend/src/services/websocket.test.ts +595 -0
- flock/frontend/src/services/websocket.ts +685 -0
- flock/frontend/src/store/filterStore.test.ts +242 -0
- flock/frontend/src/store/filterStore.ts +103 -0
- flock/frontend/src/store/graphStore.test.ts +186 -0
- flock/frontend/src/store/graphStore.ts +414 -0
- flock/frontend/src/store/moduleStore.test.ts +253 -0
- flock/frontend/src/store/moduleStore.ts +57 -0
- flock/frontend/src/store/settingsStore.ts +188 -0
- flock/frontend/src/store/streamStore.ts +68 -0
- flock/frontend/src/store/uiStore.test.ts +54 -0
- flock/frontend/src/store/uiStore.ts +110 -0
- flock/frontend/src/store/wsStore.ts +34 -0
- flock/frontend/src/styles/index.css +15 -0
- flock/frontend/src/styles/scrollbar.css +47 -0
- flock/frontend/src/styles/variables.css +488 -0
- flock/frontend/src/test/setup.ts +1 -0
- flock/frontend/src/types/filters.ts +14 -0
- flock/frontend/src/types/graph.ts +55 -0
- flock/frontend/src/types/modules.ts +7 -0
- flock/frontend/src/types/theme.ts +55 -0
- flock/frontend/src/utils/mockData.ts +85 -0
- flock/frontend/src/utils/performance.ts +16 -0
- flock/frontend/src/utils/transforms.test.ts +860 -0
- flock/frontend/src/utils/transforms.ts +323 -0
- flock/frontend/src/vite-env.d.ts +17 -0
- flock/frontend/tsconfig.json +27 -0
- flock/frontend/tsconfig.node.json +11 -0
- flock/frontend/vite.config.ts +25 -0
- flock/frontend/vitest.config.ts +11 -0
- flock/helper/cli_helper.py +1 -1
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/METADATA +1 -1
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/RECORD +117 -7
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
position: relative;
|
|
3
|
+
width: 300px;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.inputWrapper {
|
|
7
|
+
position: relative;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.input {
|
|
11
|
+
width: 100%;
|
|
12
|
+
padding: var(--spacing-2) var(--spacing-8) var(--spacing-2) var(--spacing-3);
|
|
13
|
+
background-color: var(--color-bg-surface);
|
|
14
|
+
border: 1px solid var(--color-border-default);
|
|
15
|
+
border-radius: var(--radius-md);
|
|
16
|
+
font-size: var(--font-size-body-sm);
|
|
17
|
+
font-family: var(--font-family-sans);
|
|
18
|
+
color: var(--color-text-primary);
|
|
19
|
+
outline: none;
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
transition: var(--transition-colors), box-shadow var(--duration-normal) var(--ease-smooth);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.input::placeholder {
|
|
25
|
+
color: var(--color-text-muted);
|
|
26
|
+
font-style: italic;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.input:focus {
|
|
30
|
+
border-color: var(--color-border-focus);
|
|
31
|
+
box-shadow: var(--shadow-glow-primary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.clearButton {
|
|
35
|
+
position: absolute;
|
|
36
|
+
right: var(--spacing-2);
|
|
37
|
+
top: 50%;
|
|
38
|
+
transform: translateY(-50%);
|
|
39
|
+
background: none;
|
|
40
|
+
border: none;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
font-size: 18px;
|
|
43
|
+
color: var(--color-text-muted);
|
|
44
|
+
padding: 0 var(--spacing-1);
|
|
45
|
+
line-height: 1;
|
|
46
|
+
transition: var(--transition-colors);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.clearButton:hover {
|
|
50
|
+
color: var(--color-text-secondary);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dropdown {
|
|
54
|
+
position: absolute;
|
|
55
|
+
top: 100%;
|
|
56
|
+
left: 0;
|
|
57
|
+
right: 0;
|
|
58
|
+
margin-top: var(--spacing-1);
|
|
59
|
+
background-color: var(--color-bg-surface);
|
|
60
|
+
border: 1px solid var(--color-border-default);
|
|
61
|
+
border-radius: var(--radius-md);
|
|
62
|
+
box-shadow: var(--shadow-lg);
|
|
63
|
+
max-height: 300px;
|
|
64
|
+
overflow-y: auto;
|
|
65
|
+
z-index: 1000;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.dropdownEmpty {
|
|
69
|
+
padding: var(--spacing-3);
|
|
70
|
+
color: var(--color-text-muted);
|
|
71
|
+
font-size: var(--font-size-body-sm);
|
|
72
|
+
text-align: center;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.dropdownItem {
|
|
76
|
+
padding: var(--spacing-2-5) var(--spacing-3);
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
border-bottom: 1px solid var(--color-border-subtle);
|
|
79
|
+
transition: var(--transition-colors);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dropdownItem:last-child {
|
|
83
|
+
border-bottom: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.dropdownItem:hover {
|
|
87
|
+
background-color: var(--color-bg-elevated);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.correlationId {
|
|
91
|
+
font-family: var(--font-family-mono);
|
|
92
|
+
font-size: var(--font-size-body-xs);
|
|
93
|
+
font-weight: var(--font-weight-medium);
|
|
94
|
+
color: var(--color-text-primary);
|
|
95
|
+
margin-bottom: var(--spacing-1);
|
|
96
|
+
word-break: break-all;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.metadata {
|
|
100
|
+
font-size: var(--font-size-caption);
|
|
101
|
+
color: var(--color-text-tertiary);
|
|
102
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import CorrelationIDFilter from './CorrelationIDFilter';
|
|
4
|
+
import { useFilterStore } from '../../store/filterStore';
|
|
5
|
+
|
|
6
|
+
vi.mock('../../store/filterStore');
|
|
7
|
+
|
|
8
|
+
describe('CorrelationIDFilter', () => {
|
|
9
|
+
const mockSetCorrelationId = vi.fn();
|
|
10
|
+
const mockAvailableIds = [
|
|
11
|
+
{
|
|
12
|
+
correlation_id: 'abc12345',
|
|
13
|
+
first_seen: Date.now() - 120000,
|
|
14
|
+
artifact_count: 5,
|
|
15
|
+
run_count: 2,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
correlation_id: 'def67890',
|
|
19
|
+
first_seen: Date.now() - 60000,
|
|
20
|
+
artifact_count: 3,
|
|
21
|
+
run_count: 1,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
correlation_id: 'ghi11111',
|
|
25
|
+
first_seen: Date.now() - 300000,
|
|
26
|
+
artifact_count: 10,
|
|
27
|
+
run_count: 3,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
vi.mocked(useFilterStore).mockImplementation((selector: any) => {
|
|
34
|
+
const state = {
|
|
35
|
+
correlationId: null,
|
|
36
|
+
availableCorrelationIds: mockAvailableIds,
|
|
37
|
+
setCorrelationId: mockSetCorrelationId,
|
|
38
|
+
};
|
|
39
|
+
return selector(state);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should render correlation ID filter input', () => {
|
|
44
|
+
render(<CorrelationIDFilter />);
|
|
45
|
+
expect(screen.getByPlaceholderText(/Search correlation ID/i)).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should show dropdown when input is focused', () => {
|
|
49
|
+
render(<CorrelationIDFilter />);
|
|
50
|
+
const input = screen.getByPlaceholderText(/Search correlation ID/i);
|
|
51
|
+
|
|
52
|
+
fireEvent.focus(input);
|
|
53
|
+
|
|
54
|
+
// Should show all available IDs
|
|
55
|
+
expect(screen.getByText(/abc12345/)).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByText(/def67890/)).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByText(/ghi11111/)).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should display metadata for each correlation ID', () => {
|
|
61
|
+
render(<CorrelationIDFilter />);
|
|
62
|
+
const input = screen.getByPlaceholderText(/Search correlation ID/i);
|
|
63
|
+
|
|
64
|
+
fireEvent.focus(input);
|
|
65
|
+
|
|
66
|
+
// Should show artifact count and time ago
|
|
67
|
+
expect(screen.getByText(/5 messages/)).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByText(/3 messages/)).toBeInTheDocument();
|
|
69
|
+
expect(screen.getByText(/10 messages/)).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should filter dropdown items based on input', () => {
|
|
73
|
+
render(<CorrelationIDFilter />);
|
|
74
|
+
const input = screen.getByPlaceholderText(/Search correlation ID/i) as HTMLInputElement;
|
|
75
|
+
|
|
76
|
+
fireEvent.focus(input);
|
|
77
|
+
fireEvent.change(input, { target: { value: 'abc' } });
|
|
78
|
+
|
|
79
|
+
// Should only show matching ID
|
|
80
|
+
expect(screen.getByText(/abc12345/)).toBeInTheDocument();
|
|
81
|
+
expect(screen.queryByText(/def67890/)).not.toBeInTheDocument();
|
|
82
|
+
expect(screen.queryByText(/ghi11111/)).not.toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should call setCorrelationId when an option is selected', () => {
|
|
86
|
+
render(<CorrelationIDFilter />);
|
|
87
|
+
const input = screen.getByPlaceholderText(/Search correlation ID/i);
|
|
88
|
+
|
|
89
|
+
fireEvent.focus(input);
|
|
90
|
+
const option = screen.getByText(/abc12345/);
|
|
91
|
+
fireEvent.click(option);
|
|
92
|
+
|
|
93
|
+
expect(mockSetCorrelationId).toHaveBeenCalledWith('abc12345');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should display selected correlation ID in input', () => {
|
|
97
|
+
vi.mocked(useFilterStore).mockImplementation((selector: any) => {
|
|
98
|
+
const state = {
|
|
99
|
+
correlationId: 'abc12345',
|
|
100
|
+
availableCorrelationIds: mockAvailableIds,
|
|
101
|
+
setCorrelationId: mockSetCorrelationId,
|
|
102
|
+
};
|
|
103
|
+
return selector(state);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
render(<CorrelationIDFilter />);
|
|
107
|
+
const input = screen.getByPlaceholderText(/Search correlation ID/i) as HTMLInputElement;
|
|
108
|
+
|
|
109
|
+
expect(input.value).toBe('abc12345');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should clear selection when clear button is clicked', () => {
|
|
113
|
+
vi.mocked(useFilterStore).mockImplementation((selector: any) => {
|
|
114
|
+
const state = {
|
|
115
|
+
correlationId: 'abc12345',
|
|
116
|
+
availableCorrelationIds: mockAvailableIds,
|
|
117
|
+
setCorrelationId: mockSetCorrelationId,
|
|
118
|
+
};
|
|
119
|
+
return selector(state);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
render(<CorrelationIDFilter />);
|
|
123
|
+
const clearButton = screen.getByRole('button', { name: /clear/i });
|
|
124
|
+
|
|
125
|
+
fireEvent.click(clearButton);
|
|
126
|
+
|
|
127
|
+
expect(mockSetCorrelationId).toHaveBeenCalledWith(null);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should hide dropdown when clicking outside', () => {
|
|
131
|
+
render(<CorrelationIDFilter />);
|
|
132
|
+
const input = screen.getByPlaceholderText(/Search correlation ID/i);
|
|
133
|
+
|
|
134
|
+
fireEvent.focus(input);
|
|
135
|
+
expect(screen.getByText(/abc12345/)).toBeInTheDocument();
|
|
136
|
+
|
|
137
|
+
// Simulate click outside
|
|
138
|
+
fireEvent.blur(input);
|
|
139
|
+
|
|
140
|
+
// Dropdown should be hidden (use setTimeout to wait for blur)
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
expect(screen.queryByText(/abc12345/)).not.toBeInTheDocument();
|
|
143
|
+
}, 100);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should show "No correlation IDs found" when list is empty', () => {
|
|
147
|
+
vi.mocked(useFilterStore).mockImplementation((selector: any) => {
|
|
148
|
+
const state = {
|
|
149
|
+
correlationId: null,
|
|
150
|
+
availableCorrelationIds: [],
|
|
151
|
+
setCorrelationId: mockSetCorrelationId,
|
|
152
|
+
};
|
|
153
|
+
return selector(state);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
render(<CorrelationIDFilter />);
|
|
157
|
+
const input = screen.getByPlaceholderText(/Search correlation ID/i);
|
|
158
|
+
|
|
159
|
+
fireEvent.focus(input);
|
|
160
|
+
|
|
161
|
+
expect(screen.getByText(/No correlation IDs found/i)).toBeInTheDocument();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should format time ago correctly', () => {
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
vi.mocked(useFilterStore).mockImplementation((selector: any) => {
|
|
167
|
+
const state = {
|
|
168
|
+
correlationId: null,
|
|
169
|
+
availableCorrelationIds: [
|
|
170
|
+
{
|
|
171
|
+
correlation_id: 'recent',
|
|
172
|
+
first_seen: now - 30000, // 30 seconds ago
|
|
173
|
+
artifact_count: 1,
|
|
174
|
+
run_count: 1,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
correlation_id: 'old',
|
|
178
|
+
first_seen: now - 3600000, // 1 hour ago
|
|
179
|
+
artifact_count: 1,
|
|
180
|
+
run_count: 1,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
setCorrelationId: mockSetCorrelationId,
|
|
184
|
+
};
|
|
185
|
+
return selector(state);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
render(<CorrelationIDFilter />);
|
|
189
|
+
const input = screen.getByPlaceholderText(/Search correlation ID/i);
|
|
190
|
+
|
|
191
|
+
fireEvent.focus(input);
|
|
192
|
+
|
|
193
|
+
// Should show relative time
|
|
194
|
+
expect(screen.getByText(/30s ago/i)).toBeInTheDocument();
|
|
195
|
+
expect(screen.getByText(/1h ago/i)).toBeInTheDocument();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { useFilterStore } from '../../store/filterStore';
|
|
3
|
+
import styles from './CorrelationIDFilter.module.css';
|
|
4
|
+
|
|
5
|
+
const formatTimeAgo = (timestamp: number): string => {
|
|
6
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
7
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
8
|
+
const minutes = Math.floor(seconds / 60);
|
|
9
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
10
|
+
const hours = Math.floor(minutes / 60);
|
|
11
|
+
if (hours < 24) return `${hours}h ago`;
|
|
12
|
+
const days = Math.floor(hours / 24);
|
|
13
|
+
return `${days}d ago`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const CorrelationIDFilter: React.FC = () => {
|
|
17
|
+
const correlationId = useFilterStore((state) => state.correlationId);
|
|
18
|
+
const availableCorrelationIds = useFilterStore((state) => state.availableCorrelationIds);
|
|
19
|
+
const setCorrelationId = useFilterStore((state) => state.setCorrelationId);
|
|
20
|
+
|
|
21
|
+
const [inputValue, setInputValue] = useState(correlationId || '');
|
|
22
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
23
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setInputValue(correlationId || '');
|
|
27
|
+
}, [correlationId]);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
31
|
+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
32
|
+
setIsOpen(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
37
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const filteredIds = availableCorrelationIds.filter((item) =>
|
|
41
|
+
item.correlation_id.toLowerCase().includes(inputValue.toLowerCase())
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const handleSelect = (id: string) => {
|
|
45
|
+
setCorrelationId(id);
|
|
46
|
+
setInputValue(id);
|
|
47
|
+
setIsOpen(false);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleClear = () => {
|
|
51
|
+
setCorrelationId(null);
|
|
52
|
+
setInputValue('');
|
|
53
|
+
setIsOpen(false);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
57
|
+
setInputValue(e.target.value);
|
|
58
|
+
setIsOpen(true);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleFocus = () => {
|
|
62
|
+
setIsOpen(true);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleBlur = () => {
|
|
66
|
+
// Delay to allow click on dropdown items
|
|
67
|
+
setTimeout(() => setIsOpen(false), 200);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div ref={containerRef} className={styles.container}>
|
|
72
|
+
<div className={styles.inputWrapper}>
|
|
73
|
+
<input
|
|
74
|
+
type="text"
|
|
75
|
+
value={inputValue}
|
|
76
|
+
onChange={handleInputChange}
|
|
77
|
+
onFocus={handleFocus}
|
|
78
|
+
onBlur={handleBlur}
|
|
79
|
+
placeholder="Search correlation ID..."
|
|
80
|
+
className={styles.input}
|
|
81
|
+
/>
|
|
82
|
+
{correlationId && (
|
|
83
|
+
<button
|
|
84
|
+
onClick={handleClear}
|
|
85
|
+
aria-label="Clear"
|
|
86
|
+
className={styles.clearButton}
|
|
87
|
+
>
|
|
88
|
+
×
|
|
89
|
+
</button>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{isOpen && (
|
|
94
|
+
<div className={styles.dropdown}>
|
|
95
|
+
{filteredIds.length === 0 ? (
|
|
96
|
+
<div className={styles.dropdownEmpty}>
|
|
97
|
+
No correlation IDs found
|
|
98
|
+
</div>
|
|
99
|
+
) : (
|
|
100
|
+
filteredIds.map((item) => (
|
|
101
|
+
<div
|
|
102
|
+
key={item.correlation_id}
|
|
103
|
+
onClick={() => handleSelect(item.correlation_id)}
|
|
104
|
+
className={styles.dropdownItem}
|
|
105
|
+
>
|
|
106
|
+
<div className={styles.correlationId}>
|
|
107
|
+
{item.correlation_id}
|
|
108
|
+
</div>
|
|
109
|
+
<div className={styles.metadata}>
|
|
110
|
+
{item.artifact_count} messages, {formatTimeAgo(item.first_seen)}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
))
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export default CorrelationIDFilter;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.filterBar {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--space-layout-sm);
|
|
5
|
+
padding: var(--space-layout-sm) var(--space-layout-md);
|
|
6
|
+
background-color: var(--color-bg-elevated);
|
|
7
|
+
border-bottom: 1px solid var(--color-border-subtle);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.filterControls {
|
|
11
|
+
display: flex;
|
|
12
|
+
gap: var(--space-layout-sm);
|
|
13
|
+
align-items: flex-start;
|
|
14
|
+
flex-wrap: wrap;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.filterGroup {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: var(--gap-md);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.filterLabel {
|
|
24
|
+
font-size: var(--font-size-overline);
|
|
25
|
+
font-weight: var(--font-weight-semibold);
|
|
26
|
+
color: var(--color-text-tertiary);
|
|
27
|
+
text-transform: uppercase;
|
|
28
|
+
letter-spacing: var(--letter-spacing-wider);
|
|
29
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import FilterBar from './FilterBar';
|
|
4
|
+
import { useFilterStore } from '../../store/filterStore';
|
|
5
|
+
|
|
6
|
+
vi.mock('../../store/filterStore');
|
|
7
|
+
vi.mock('./CorrelationIDFilter', () => ({
|
|
8
|
+
default: () => <div data-testid="correlation-id-filter">CorrelationIDFilter</div>,
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('./TimeRangeFilter', () => ({
|
|
11
|
+
default: () => <div data-testid="time-range-filter">TimeRangeFilter</div>,
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('./FilterPills', () => ({
|
|
14
|
+
default: () => <div data-testid="filter-pills">FilterPills</div>,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('FilterBar', () => {
|
|
18
|
+
it('should render all filter components', () => {
|
|
19
|
+
vi.mocked(useFilterStore).mockImplementation((selector: any) => {
|
|
20
|
+
const state = {
|
|
21
|
+
correlationId: null,
|
|
22
|
+
timeRange: { preset: 'last10min' },
|
|
23
|
+
availableCorrelationIds: [],
|
|
24
|
+
getActiveFilters: () => [],
|
|
25
|
+
};
|
|
26
|
+
return selector(state);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
render(<FilterBar />);
|
|
30
|
+
|
|
31
|
+
expect(screen.getByTestId('correlation-id-filter')).toBeInTheDocument();
|
|
32
|
+
expect(screen.getByTestId('time-range-filter')).toBeInTheDocument();
|
|
33
|
+
expect(screen.getByTestId('filter-pills')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should have proper layout structure', () => {
|
|
37
|
+
vi.mocked(useFilterStore).mockImplementation((selector: any) => {
|
|
38
|
+
const state = {
|
|
39
|
+
correlationId: null,
|
|
40
|
+
timeRange: { preset: 'last10min' },
|
|
41
|
+
availableCorrelationIds: [],
|
|
42
|
+
getActiveFilters: () => [],
|
|
43
|
+
};
|
|
44
|
+
return selector(state);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const { container } = render(<FilterBar />);
|
|
48
|
+
|
|
49
|
+
// Should have a container with CSS module class (hashed)
|
|
50
|
+
const filterBar = container.firstChild as HTMLElement;
|
|
51
|
+
expect(filterBar.className).toMatch(/filterBar/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should render correlation ID filter and time range filter in top row', () => {
|
|
55
|
+
vi.mocked(useFilterStore).mockImplementation((selector: any) => {
|
|
56
|
+
const state = {
|
|
57
|
+
correlationId: null,
|
|
58
|
+
timeRange: { preset: 'last10min' },
|
|
59
|
+
availableCorrelationIds: [],
|
|
60
|
+
getActiveFilters: () => [],
|
|
61
|
+
};
|
|
62
|
+
return selector(state);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
render(<FilterBar />);
|
|
66
|
+
|
|
67
|
+
const correlationFilter = screen.getByTestId('correlation-id-filter');
|
|
68
|
+
const timeRangeFilter = screen.getByTestId('time-range-filter');
|
|
69
|
+
|
|
70
|
+
// Both should be present
|
|
71
|
+
expect(correlationFilter).toBeInTheDocument();
|
|
72
|
+
expect(timeRangeFilter).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should render filter pills below filter controls', () => {
|
|
76
|
+
vi.mocked(useFilterStore).mockImplementation((selector: any) => {
|
|
77
|
+
const state = {
|
|
78
|
+
correlationId: 'test-123',
|
|
79
|
+
timeRange: { preset: 'last5min' },
|
|
80
|
+
availableCorrelationIds: [],
|
|
81
|
+
getActiveFilters: () => [
|
|
82
|
+
{
|
|
83
|
+
type: 'correlationId',
|
|
84
|
+
value: 'test-123',
|
|
85
|
+
label: 'Correlation ID: test-123',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
return selector(state);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
render(<FilterBar />);
|
|
93
|
+
|
|
94
|
+
expect(screen.getByTestId('filter-pills')).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should have appropriate spacing between components', () => {
|
|
98
|
+
vi.mocked(useFilterStore).mockImplementation((selector: any) => {
|
|
99
|
+
const state = {
|
|
100
|
+
correlationId: null,
|
|
101
|
+
timeRange: { preset: 'last10min' },
|
|
102
|
+
availableCorrelationIds: [],
|
|
103
|
+
getActiveFilters: () => [],
|
|
104
|
+
};
|
|
105
|
+
return selector(state);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const { container } = render(<FilterBar />);
|
|
109
|
+
const filterBar = container.firstChild as HTMLElement;
|
|
110
|
+
|
|
111
|
+
// Should have filter controls with CSS module class (hashed)
|
|
112
|
+
const filterControls = filterBar.querySelector('[class*="filterControls"]');
|
|
113
|
+
expect(filterControls).toBeInTheDocument();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should maintain consistent styling with dashboard theme', () => {
|
|
117
|
+
vi.mocked(useFilterStore).mockImplementation((selector: any) => {
|
|
118
|
+
const state = {
|
|
119
|
+
correlationId: null,
|
|
120
|
+
timeRange: { preset: 'last10min' },
|
|
121
|
+
availableCorrelationIds: [],
|
|
122
|
+
getActiveFilters: () => [],
|
|
123
|
+
};
|
|
124
|
+
return selector(state);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const { container } = render(<FilterBar />);
|
|
128
|
+
const filterBar = container.firstChild as HTMLElement;
|
|
129
|
+
|
|
130
|
+
// Should have padding and background consistent with dashboard
|
|
131
|
+
expect(filterBar).toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import CorrelationIDFilter from './CorrelationIDFilter';
|
|
3
|
+
import TimeRangeFilter from './TimeRangeFilter';
|
|
4
|
+
import FilterPills from './FilterPills';
|
|
5
|
+
import styles from './FilterBar.module.css';
|
|
6
|
+
|
|
7
|
+
const FilterBar: React.FC = () => {
|
|
8
|
+
return (
|
|
9
|
+
<div className={styles.filterBar}>
|
|
10
|
+
{/* Filter Controls */}
|
|
11
|
+
<div className={styles.filterControls}>
|
|
12
|
+
<div className={styles.filterGroup}>
|
|
13
|
+
<label className={styles.filterLabel}>
|
|
14
|
+
Correlation ID
|
|
15
|
+
</label>
|
|
16
|
+
<CorrelationIDFilter />
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div className={styles.filterGroup}>
|
|
20
|
+
<label className={styles.filterLabel}>
|
|
21
|
+
Time Range
|
|
22
|
+
</label>
|
|
23
|
+
<TimeRangeFilter />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{/* Active Filter Pills */}
|
|
28
|
+
<FilterPills />
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default FilterBar;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
display: flex;
|
|
3
|
+
gap: var(--spacing-2);
|
|
4
|
+
align-items: center;
|
|
5
|
+
flex-wrap: wrap;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.pill {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: var(--gap-md);
|
|
12
|
+
padding: var(--spacing-1-5) var(--spacing-3);
|
|
13
|
+
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
|
|
14
|
+
border: 1px solid var(--color-primary-500);
|
|
15
|
+
border-radius: var(--radius-full);
|
|
16
|
+
font-size: var(--font-size-body-xs);
|
|
17
|
+
font-weight: var(--font-weight-medium);
|
|
18
|
+
color: var(--color-text-on-primary);
|
|
19
|
+
box-shadow: var(--shadow-xs);
|
|
20
|
+
transition: var(--transition-all);
|
|
21
|
+
animation: slideIn var(--duration-normal) var(--ease-out);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.pill:hover {
|
|
25
|
+
transform: translateY(-1px);
|
|
26
|
+
box-shadow: var(--shadow-sm);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.pillSecondary {
|
|
30
|
+
background: linear-gradient(135deg, var(--color-secondary-600) 0%, var(--color-secondary-700) 100%);
|
|
31
|
+
border-color: var(--color-secondary-500);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.pillLabel {
|
|
35
|
+
user-select: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.removeButton {
|
|
39
|
+
background: none;
|
|
40
|
+
border: none;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
padding: 0 var(--spacing-0-5);
|
|
43
|
+
font-size: 16px;
|
|
44
|
+
color: var(--color-text-on-primary);
|
|
45
|
+
line-height: 1;
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
transition: var(--transition-transform);
|
|
50
|
+
opacity: 0.8;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.removeButton:hover {
|
|
54
|
+
opacity: 1;
|
|
55
|
+
transform: scale(1.2);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@keyframes slideIn {
|
|
59
|
+
from {
|
|
60
|
+
opacity: 0;
|
|
61
|
+
transform: translateX(-10px);
|
|
62
|
+
}
|
|
63
|
+
to {
|
|
64
|
+
opacity: 1;
|
|
65
|
+
transform: translateX(0);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Animation when pill is removed */
|
|
70
|
+
.pill.removing {
|
|
71
|
+
animation: slideOut var(--duration-fast) var(--ease-in) forwards;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@keyframes slideOut {
|
|
75
|
+
to {
|
|
76
|
+
opacity: 0;
|
|
77
|
+
transform: scale(0.8) translateX(-10px);
|
|
78
|
+
}
|
|
79
|
+
}
|