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.

Files changed (117) hide show
  1. flock/dashboard/launcher.py +1 -1
  2. flock/frontend/README.md +678 -0
  3. flock/frontend/docs/DESIGN_SYSTEM.md +1980 -0
  4. flock/frontend/index.html +12 -0
  5. flock/frontend/package-lock.json +4347 -0
  6. flock/frontend/package.json +48 -0
  7. flock/frontend/src/App.tsx +79 -0
  8. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +587 -0
  9. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +387 -0
  10. flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +640 -0
  11. flock/frontend/src/__tests__/integration/indexeddb-persistence.test.tsx +699 -0
  12. flock/frontend/src/components/common/BuildInfo.tsx +39 -0
  13. flock/frontend/src/components/common/EmptyState.module.css +115 -0
  14. flock/frontend/src/components/common/EmptyState.tsx +128 -0
  15. flock/frontend/src/components/common/ErrorBoundary.module.css +169 -0
  16. flock/frontend/src/components/common/ErrorBoundary.tsx +118 -0
  17. flock/frontend/src/components/common/KeyboardShortcutsDialog.css +251 -0
  18. flock/frontend/src/components/common/KeyboardShortcutsDialog.tsx +151 -0
  19. flock/frontend/src/components/common/LoadingSpinner.module.css +97 -0
  20. flock/frontend/src/components/common/LoadingSpinner.tsx +29 -0
  21. flock/frontend/src/components/controls/PublishControl.css +547 -0
  22. flock/frontend/src/components/controls/PublishControl.test.tsx +543 -0
  23. flock/frontend/src/components/controls/PublishControl.tsx +432 -0
  24. flock/frontend/src/components/details/DetailWindowContainer.tsx +62 -0
  25. flock/frontend/src/components/details/LiveOutputTab.test.tsx +792 -0
  26. flock/frontend/src/components/details/LiveOutputTab.tsx +220 -0
  27. flock/frontend/src/components/details/MessageHistoryTab.tsx +299 -0
  28. flock/frontend/src/components/details/NodeDetailWindow.test.tsx +501 -0
  29. flock/frontend/src/components/details/NodeDetailWindow.tsx +218 -0
  30. flock/frontend/src/components/details/RunStatusTab.tsx +307 -0
  31. flock/frontend/src/components/details/tabs.test.tsx +1015 -0
  32. flock/frontend/src/components/filters/CorrelationIDFilter.module.css +102 -0
  33. flock/frontend/src/components/filters/CorrelationIDFilter.test.tsx +197 -0
  34. flock/frontend/src/components/filters/CorrelationIDFilter.tsx +121 -0
  35. flock/frontend/src/components/filters/FilterBar.module.css +29 -0
  36. flock/frontend/src/components/filters/FilterBar.test.tsx +133 -0
  37. flock/frontend/src/components/filters/FilterBar.tsx +33 -0
  38. flock/frontend/src/components/filters/FilterPills.module.css +79 -0
  39. flock/frontend/src/components/filters/FilterPills.test.tsx +173 -0
  40. flock/frontend/src/components/filters/FilterPills.tsx +67 -0
  41. flock/frontend/src/components/filters/TimeRangeFilter.module.css +91 -0
  42. flock/frontend/src/components/filters/TimeRangeFilter.test.tsx +154 -0
  43. flock/frontend/src/components/filters/TimeRangeFilter.tsx +105 -0
  44. flock/frontend/src/components/graph/AgentNode.test.tsx +75 -0
  45. flock/frontend/src/components/graph/AgentNode.tsx +322 -0
  46. flock/frontend/src/components/graph/GraphCanvas.tsx +406 -0
  47. flock/frontend/src/components/graph/MessageFlowEdge.tsx +128 -0
  48. flock/frontend/src/components/graph/MessageNode.test.tsx +62 -0
  49. flock/frontend/src/components/graph/MessageNode.tsx +116 -0
  50. flock/frontend/src/components/graph/MiniMap.tsx +47 -0
  51. flock/frontend/src/components/graph/TransformEdge.tsx +123 -0
  52. flock/frontend/src/components/layout/DashboardLayout.css +407 -0
  53. flock/frontend/src/components/layout/DashboardLayout.tsx +300 -0
  54. flock/frontend/src/components/layout/Header.module.css +88 -0
  55. flock/frontend/src/components/layout/Header.tsx +52 -0
  56. flock/frontend/src/components/modules/EventLogModule.test.tsx +401 -0
  57. flock/frontend/src/components/modules/EventLogModule.tsx +396 -0
  58. flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +17 -0
  59. flock/frontend/src/components/modules/ModuleRegistry.test.ts +333 -0
  60. flock/frontend/src/components/modules/ModuleRegistry.ts +85 -0
  61. flock/frontend/src/components/modules/ModuleWindow.tsx +155 -0
  62. flock/frontend/src/components/modules/registerModules.ts +20 -0
  63. flock/frontend/src/components/settings/AdvancedSettings.tsx +175 -0
  64. flock/frontend/src/components/settings/AppearanceSettings.tsx +185 -0
  65. flock/frontend/src/components/settings/GraphSettings.tsx +110 -0
  66. flock/frontend/src/components/settings/SettingsPanel.css +327 -0
  67. flock/frontend/src/components/settings/SettingsPanel.tsx +131 -0
  68. flock/frontend/src/components/settings/ThemeSelector.tsx +298 -0
  69. flock/frontend/src/hooks/useKeyboardShortcuts.ts +148 -0
  70. flock/frontend/src/hooks/useModulePersistence.test.ts +442 -0
  71. flock/frontend/src/hooks/useModulePersistence.ts +154 -0
  72. flock/frontend/src/hooks/useModules.ts +139 -0
  73. flock/frontend/src/hooks/usePersistence.ts +139 -0
  74. flock/frontend/src/main.tsx +13 -0
  75. flock/frontend/src/services/api.ts +213 -0
  76. flock/frontend/src/services/indexeddb.test.ts +793 -0
  77. flock/frontend/src/services/indexeddb.ts +794 -0
  78. flock/frontend/src/services/layout.test.ts +437 -0
  79. flock/frontend/src/services/layout.ts +146 -0
  80. flock/frontend/src/services/themeApplicator.ts +140 -0
  81. flock/frontend/src/services/themeService.ts +77 -0
  82. flock/frontend/src/services/websocket.test.ts +595 -0
  83. flock/frontend/src/services/websocket.ts +685 -0
  84. flock/frontend/src/store/filterStore.test.ts +242 -0
  85. flock/frontend/src/store/filterStore.ts +103 -0
  86. flock/frontend/src/store/graphStore.test.ts +186 -0
  87. flock/frontend/src/store/graphStore.ts +414 -0
  88. flock/frontend/src/store/moduleStore.test.ts +253 -0
  89. flock/frontend/src/store/moduleStore.ts +57 -0
  90. flock/frontend/src/store/settingsStore.ts +188 -0
  91. flock/frontend/src/store/streamStore.ts +68 -0
  92. flock/frontend/src/store/uiStore.test.ts +54 -0
  93. flock/frontend/src/store/uiStore.ts +110 -0
  94. flock/frontend/src/store/wsStore.ts +34 -0
  95. flock/frontend/src/styles/index.css +15 -0
  96. flock/frontend/src/styles/scrollbar.css +47 -0
  97. flock/frontend/src/styles/variables.css +488 -0
  98. flock/frontend/src/test/setup.ts +1 -0
  99. flock/frontend/src/types/filters.ts +14 -0
  100. flock/frontend/src/types/graph.ts +55 -0
  101. flock/frontend/src/types/modules.ts +7 -0
  102. flock/frontend/src/types/theme.ts +55 -0
  103. flock/frontend/src/utils/mockData.ts +85 -0
  104. flock/frontend/src/utils/performance.ts +16 -0
  105. flock/frontend/src/utils/transforms.test.ts +860 -0
  106. flock/frontend/src/utils/transforms.ts +323 -0
  107. flock/frontend/src/vite-env.d.ts +17 -0
  108. flock/frontend/tsconfig.json +27 -0
  109. flock/frontend/tsconfig.node.json +11 -0
  110. flock/frontend/vite.config.ts +25 -0
  111. flock/frontend/vitest.config.ts +11 -0
  112. flock/helper/cli_helper.py +1 -1
  113. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/METADATA +1 -1
  114. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/RECORD +117 -7
  115. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/WHEEL +0 -0
  116. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/entry_points.txt +0 -0
  117. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,173 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import FilterPills from './FilterPills';
4
+ import { useFilterStore } from '../../store/filterStore';
5
+
6
+ vi.mock('../../store/filterStore');
7
+
8
+ describe('FilterPills', () => {
9
+ const mockRemoveFilter = vi.fn();
10
+
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ });
14
+
15
+ it('should not render anything when no filters are active', () => {
16
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
17
+ const state = {
18
+ correlationId: null,
19
+ timeRange: { preset: 'last10min' as const },
20
+ removeFilter: mockRemoveFilter,
21
+ };
22
+ return selector(state);
23
+ });
24
+
25
+ const { container } = render(<FilterPills />);
26
+ expect(container.firstChild).toBeNull();
27
+ });
28
+
29
+ it('should render filter pill for active correlation ID', () => {
30
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
31
+ const state = {
32
+ correlationId: 'test-123',
33
+ timeRange: { preset: 'last10min' as const },
34
+ removeFilter: mockRemoveFilter,
35
+ };
36
+ return selector(state);
37
+ });
38
+
39
+ render(<FilterPills />);
40
+ expect(screen.getByText('Correlation ID: test-123')).toBeInTheDocument();
41
+ });
42
+
43
+ it('should render filter pill for active time range', () => {
44
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
45
+ const state = {
46
+ correlationId: null,
47
+ timeRange: { preset: 'last5min' as const },
48
+ removeFilter: mockRemoveFilter,
49
+ };
50
+ return selector(state);
51
+ });
52
+
53
+ render(<FilterPills />);
54
+ expect(screen.getByText('Time: Last 5 min')).toBeInTheDocument();
55
+ });
56
+
57
+ it('should render multiple filter pills', () => {
58
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
59
+ const state = {
60
+ correlationId: 'test-123',
61
+ timeRange: { preset: 'last1hour' as const },
62
+ removeFilter: mockRemoveFilter,
63
+ };
64
+ return selector(state);
65
+ });
66
+
67
+ render(<FilterPills />);
68
+ expect(screen.getByText('Correlation ID: test-123')).toBeInTheDocument();
69
+ expect(screen.getByText('Time: Last hour')).toBeInTheDocument();
70
+ });
71
+
72
+ it('should render remove button for each pill', () => {
73
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
74
+ const state = {
75
+ correlationId: 'test-123',
76
+ timeRange: { preset: 'last10min' as const },
77
+ removeFilter: mockRemoveFilter,
78
+ };
79
+ return selector(state);
80
+ });
81
+
82
+ render(<FilterPills />);
83
+ const removeButton = screen.getByRole('button', { name: /remove.*correlation/i });
84
+ expect(removeButton).toBeInTheDocument();
85
+ });
86
+
87
+ it('should call removeFilter when remove button is clicked', () => {
88
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
89
+ const state = {
90
+ correlationId: 'test-123',
91
+ timeRange: { preset: 'last10min' as const },
92
+ removeFilter: mockRemoveFilter,
93
+ };
94
+ return selector(state);
95
+ });
96
+
97
+ render(<FilterPills />);
98
+ const removeButton = screen.getByRole('button', { name: /remove.*correlation/i });
99
+
100
+ fireEvent.click(removeButton);
101
+
102
+ expect(mockRemoveFilter).toHaveBeenCalledWith('correlationId');
103
+ });
104
+
105
+ it('should call removeFilter with correct type for time range', () => {
106
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
107
+ const state = {
108
+ correlationId: null,
109
+ timeRange: { preset: 'last5min' as const },
110
+ removeFilter: mockRemoveFilter,
111
+ };
112
+ return selector(state);
113
+ });
114
+
115
+ render(<FilterPills />);
116
+ const removeButton = screen.getByRole('button', { name: /remove.*time/i });
117
+
118
+ fireEvent.click(removeButton);
119
+
120
+ expect(mockRemoveFilter).toHaveBeenCalledWith('timeRange');
121
+ });
122
+
123
+ it('should display pills in a horizontal layout', () => {
124
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
125
+ const state = {
126
+ correlationId: 'test-123',
127
+ timeRange: { preset: 'last5min' as const },
128
+ removeFilter: mockRemoveFilter,
129
+ };
130
+ return selector(state);
131
+ });
132
+
133
+ render(<FilterPills />);
134
+ const container = screen.getByText('Correlation ID: test-123').closest('div')?.parentElement;
135
+
136
+ // Should have container class (hashed by CSS modules)
137
+ expect(container?.className).toMatch(/container/);
138
+ });
139
+
140
+ it('should render X icon in remove button', () => {
141
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
142
+ const state = {
143
+ correlationId: 'test-123',
144
+ timeRange: { preset: 'last10min' as const },
145
+ removeFilter: mockRemoveFilter,
146
+ };
147
+ return selector(state);
148
+ });
149
+
150
+ render(<FilterPills />);
151
+ const removeButton = screen.getByRole('button', { name: /remove.*correlation/i });
152
+
153
+ expect(removeButton).toHaveTextContent('×');
154
+ });
155
+
156
+ it('should handle custom time range label', () => {
157
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
158
+ const state = {
159
+ correlationId: null,
160
+ timeRange: {
161
+ preset: 'custom' as const,
162
+ start: new Date('2025-01-01T10:00:00').getTime(),
163
+ end: new Date('2025-01-01T12:00:00').getTime(),
164
+ },
165
+ removeFilter: mockRemoveFilter,
166
+ };
167
+ return selector(state);
168
+ });
169
+
170
+ render(<FilterPills />);
171
+ expect(screen.getByText(/Time:/)).toBeInTheDocument();
172
+ });
173
+ });
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { useFilterStore } from '../../store/filterStore';
3
+ import styles from './FilterPills.module.css';
4
+
5
+ const FilterPills: React.FC = () => {
6
+ // Select the actual state values for reactivity, then derive filters
7
+ const correlationId = useFilterStore((state) => state.correlationId);
8
+ const timeRange = useFilterStore((state) => state.timeRange);
9
+ const removeFilter = useFilterStore((state) => state.removeFilter);
10
+
11
+ // Derive active filters from state
12
+ const activeFilters: Array<{ type: 'correlationId' | 'timeRange'; value: any; label: string }> = [];
13
+
14
+ if (correlationId) {
15
+ activeFilters.push({
16
+ type: 'correlationId',
17
+ value: correlationId,
18
+ label: `Correlation ID: ${correlationId}`,
19
+ });
20
+ }
21
+
22
+ if (timeRange.preset !== 'last10min') {
23
+ const formatTimeRange = (range: typeof timeRange): string => {
24
+ if (range.preset === 'last5min') return 'Last 5 min';
25
+ if (range.preset === 'last10min') return 'Last 10 min';
26
+ if (range.preset === 'last1hour') return 'Last hour';
27
+ if (range.preset === 'custom' && range.start && range.end) {
28
+ const startDate = new Date(range.start).toLocaleString();
29
+ const endDate = new Date(range.end).toLocaleString();
30
+ return `${startDate} - ${endDate}`;
31
+ }
32
+ return 'Unknown';
33
+ };
34
+
35
+ activeFilters.push({
36
+ type: 'timeRange',
37
+ value: timeRange,
38
+ label: `Time: ${formatTimeRange(timeRange)}`,
39
+ });
40
+ }
41
+
42
+ if (activeFilters.length === 0) {
43
+ return null;
44
+ }
45
+
46
+ return (
47
+ <div className={styles.container}>
48
+ {activeFilters.map((filter, index) => (
49
+ <div
50
+ key={filter.type}
51
+ className={`${styles.pill} ${index === 1 ? styles.pillSecondary : ''}`}
52
+ >
53
+ <span className={styles.pillLabel}>{filter.label}</span>
54
+ <button
55
+ onClick={() => removeFilter(filter.type)}
56
+ aria-label={`Remove ${filter.type} filter`}
57
+ className={styles.removeButton}
58
+ >
59
+ ×
60
+ </button>
61
+ </div>
62
+ ))}
63
+ </div>
64
+ );
65
+ };
66
+
67
+ export default FilterPills;
@@ -0,0 +1,91 @@
1
+ .container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--spacing-2);
5
+ }
6
+
7
+ .presetButtons {
8
+ display: flex;
9
+ gap: var(--spacing-2);
10
+ align-items: center;
11
+ }
12
+
13
+ .presetButton {
14
+ padding: var(--spacing-2) var(--spacing-4);
15
+ background-color: var(--color-bg-surface);
16
+ color: var(--color-text-secondary);
17
+ border: 1px solid var(--color-border-default);
18
+ border-radius: var(--radius-md);
19
+ cursor: pointer;
20
+ font-size: var(--font-size-body-sm);
21
+ font-weight: var(--font-weight-medium);
22
+ font-family: var(--font-family-sans);
23
+ transition: var(--transition-colors);
24
+ white-space: nowrap;
25
+ }
26
+
27
+ .presetButton:hover {
28
+ background-color: var(--color-bg-overlay);
29
+ border-color: var(--color-border-strong);
30
+ }
31
+
32
+ .presetButton.active {
33
+ background-color: var(--color-primary-600);
34
+ color: var(--color-text-on-primary);
35
+ border-color: var(--color-primary-600);
36
+ }
37
+
38
+ .presetButton.active:hover {
39
+ background-color: var(--color-primary-700);
40
+ border-color: var(--color-primary-700);
41
+ }
42
+
43
+ .customRange {
44
+ display: flex;
45
+ gap: var(--spacing-3);
46
+ align-items: center;
47
+ padding: var(--spacing-2);
48
+ background-color: var(--color-bg-surface);
49
+ border-radius: var(--radius-md);
50
+ border: 1px solid var(--color-border-subtle);
51
+ }
52
+
53
+ .dateInputGroup {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: var(--spacing-1);
57
+ }
58
+
59
+ .dateLabel {
60
+ font-size: var(--font-size-caption);
61
+ color: var(--color-text-tertiary);
62
+ font-weight: var(--font-weight-medium);
63
+ }
64
+
65
+ .dateInput {
66
+ padding: var(--spacing-1-5) var(--spacing-2);
67
+ background-color: var(--color-bg-elevated);
68
+ border: 1px solid var(--color-border-default);
69
+ border-radius: var(--radius-md);
70
+ font-size: var(--font-size-body-sm);
71
+ font-family: var(--font-family-sans);
72
+ color: var(--color-text-primary);
73
+ outline: none;
74
+ transition: var(--transition-colors), box-shadow var(--duration-normal) var(--ease-smooth);
75
+ }
76
+
77
+ .dateInput:focus {
78
+ border-color: var(--color-border-focus);
79
+ box-shadow: var(--shadow-glow-primary);
80
+ }
81
+
82
+ /* Custom styling for date input calendar icon */
83
+ .dateInput::-webkit-calendar-picker-indicator {
84
+ cursor: pointer;
85
+ filter: invert(0.7);
86
+ transition: var(--transition-opacity);
87
+ }
88
+
89
+ .dateInput::-webkit-calendar-picker-indicator:hover {
90
+ filter: invert(1);
91
+ }
@@ -0,0 +1,154 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import TimeRangeFilter from './TimeRangeFilter';
4
+ import { useFilterStore } from '../../store/filterStore';
5
+
6
+ vi.mock('../../store/filterStore');
7
+
8
+ describe('TimeRangeFilter', () => {
9
+ const mockSetTimeRange = vi.fn();
10
+
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
14
+ const state = {
15
+ timeRange: { preset: 'last10min' },
16
+ setTimeRange: mockSetTimeRange,
17
+ };
18
+ return selector(state);
19
+ });
20
+ });
21
+
22
+ it('should render time range filter with presets', () => {
23
+ render(<TimeRangeFilter />);
24
+ expect(screen.getByText(/Last 5 min/i)).toBeInTheDocument();
25
+ expect(screen.getByText(/Last 10 min/i)).toBeInTheDocument();
26
+ expect(screen.getByText(/Last hour/i)).toBeInTheDocument();
27
+ expect(screen.getByText(/Custom/i)).toBeInTheDocument();
28
+ });
29
+
30
+ it('should highlight selected preset', () => {
31
+ render(<TimeRangeFilter />);
32
+ const last10MinButton = screen.getByText(/Last 10 min/i);
33
+
34
+ // Should have active class for selected preset (hashed by CSS modules)
35
+ expect(last10MinButton.className).toMatch(/active/);
36
+ });
37
+
38
+ it('should call setTimeRange when preset is selected', () => {
39
+ render(<TimeRangeFilter />);
40
+ const last5MinButton = screen.getByText(/Last 5 min/i);
41
+
42
+ fireEvent.click(last5MinButton);
43
+
44
+ expect(mockSetTimeRange).toHaveBeenCalledWith({ preset: 'last5min' });
45
+ });
46
+
47
+ it('should show custom date inputs when Custom is selected', () => {
48
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
49
+ const state = {
50
+ timeRange: { preset: 'custom', start: Date.now() - 3600000, end: Date.now() },
51
+ setTimeRange: mockSetTimeRange,
52
+ };
53
+ return selector(state);
54
+ });
55
+
56
+ render(<TimeRangeFilter />);
57
+
58
+ expect(screen.getByLabelText(/Start/i)).toBeInTheDocument();
59
+ expect(screen.getByLabelText(/End/i)).toBeInTheDocument();
60
+ });
61
+
62
+ it('should not show custom date inputs when preset is not custom', () => {
63
+ render(<TimeRangeFilter />);
64
+
65
+ expect(screen.queryByLabelText(/Start/i)).not.toBeInTheDocument();
66
+ expect(screen.queryByLabelText(/End/i)).not.toBeInTheDocument();
67
+ });
68
+
69
+ it('should call setTimeRange with custom range when dates are changed', () => {
70
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
71
+ const state = {
72
+ timeRange: { preset: 'custom', start: Date.now() - 3600000, end: Date.now() },
73
+ setTimeRange: mockSetTimeRange,
74
+ };
75
+ return selector(state);
76
+ });
77
+
78
+ render(<TimeRangeFilter />);
79
+
80
+ const startInput = screen.getByLabelText(/Start/i) as HTMLInputElement;
81
+
82
+ // Create datetime-local format
83
+ const startDate = new Date(Date.now() - 7200000);
84
+ const startValue = startDate.toISOString().slice(0, 16);
85
+
86
+ fireEvent.change(startInput, { target: { value: startValue } });
87
+
88
+ expect(mockSetTimeRange).toHaveBeenCalledWith({
89
+ preset: 'custom',
90
+ start: expect.any(Number),
91
+ end: expect.any(Number),
92
+ });
93
+ });
94
+
95
+ it('should display current custom range values in inputs', () => {
96
+ const now = Date.now();
97
+ const start = now - 3600000;
98
+ const end = now;
99
+
100
+ vi.mocked(useFilterStore).mockImplementation((selector: any) => {
101
+ const state = {
102
+ timeRange: { preset: 'custom', start, end },
103
+ setTimeRange: mockSetTimeRange,
104
+ };
105
+ return selector(state);
106
+ });
107
+
108
+ render(<TimeRangeFilter />);
109
+
110
+ const startInput = screen.getByLabelText(/Start/i) as HTMLInputElement;
111
+ const endInput = screen.getByLabelText(/End/i) as HTMLInputElement;
112
+
113
+ // Should have values set
114
+ expect(startInput.value).toBeTruthy();
115
+ expect(endInput.value).toBeTruthy();
116
+ });
117
+
118
+ it('should switch to custom preset when clicking Custom button', () => {
119
+ render(<TimeRangeFilter />);
120
+ const customButton = screen.getByText(/Custom/i);
121
+
122
+ fireEvent.click(customButton);
123
+
124
+ expect(mockSetTimeRange).toHaveBeenCalledWith({
125
+ preset: 'custom',
126
+ start: expect.any(Number),
127
+ end: expect.any(Number),
128
+ });
129
+ });
130
+
131
+ it('should apply consistent styling to all preset buttons', () => {
132
+ render(<TimeRangeFilter />);
133
+ const buttons = [
134
+ screen.getByText(/Last 5 min/i),
135
+ screen.getByText(/Last 10 min/i),
136
+ screen.getByText(/Last hour/i),
137
+ screen.getByText(/Custom/i),
138
+ ];
139
+
140
+ buttons.forEach((button) => {
141
+ // All buttons should have the presetButton class (hashed by CSS modules)
142
+ expect(button.className).toMatch(/presetButton/);
143
+ });
144
+ });
145
+
146
+ it('should handle last1hour preset correctly', () => {
147
+ render(<TimeRangeFilter />);
148
+ const lastHourButton = screen.getByText(/Last hour/i);
149
+
150
+ fireEvent.click(lastHourButton);
151
+
152
+ expect(mockSetTimeRange).toHaveBeenCalledWith({ preset: 'last1hour' });
153
+ });
154
+ });
@@ -0,0 +1,105 @@
1
+ import React from 'react';
2
+ import { useFilterStore } from '../../store/filterStore';
3
+ import { TimeRangePreset } from '../../types/filters';
4
+ import styles from './TimeRangeFilter.module.css';
5
+
6
+ const TimeRangeFilter: React.FC = () => {
7
+ const timeRange = useFilterStore((state) => state.timeRange);
8
+ const setTimeRange = useFilterStore((state) => state.setTimeRange);
9
+
10
+ const handlePresetClick = (preset: TimeRangePreset) => {
11
+ if (preset === 'custom') {
12
+ // Initialize custom range with last hour
13
+ const end = Date.now();
14
+ const start = end - 3600000; // 1 hour ago
15
+ setTimeRange({ preset: 'custom', start, end });
16
+ } else {
17
+ setTimeRange({ preset });
18
+ }
19
+ };
20
+
21
+ const handleStartChange = (e: React.ChangeEvent<HTMLInputElement>) => {
22
+ const start = new Date(e.target.value).getTime();
23
+ setTimeRange({
24
+ preset: 'custom',
25
+ start,
26
+ end: timeRange.end || Date.now(),
27
+ });
28
+ };
29
+
30
+ const handleEndChange = (e: React.ChangeEvent<HTMLInputElement>) => {
31
+ const end = new Date(e.target.value).getTime();
32
+ setTimeRange({
33
+ preset: 'custom',
34
+ start: timeRange.start || Date.now() - 3600000,
35
+ end,
36
+ });
37
+ };
38
+
39
+ const formatDateTimeLocal = (timestamp: number): string => {
40
+ return new Date(timestamp).toISOString().slice(0, 16);
41
+ };
42
+
43
+ const presets: { preset: TimeRangePreset; label: string }[] = [
44
+ { preset: 'last5min', label: 'Last 5 min' },
45
+ { preset: 'last10min', label: 'Last 10 min' },
46
+ { preset: 'last1hour', label: 'Last hour' },
47
+ { preset: 'custom', label: 'Custom' },
48
+ ];
49
+
50
+ return (
51
+ <div className={styles.container}>
52
+ <div className={styles.presetButtons}>
53
+ {presets.map(({ preset, label }) => (
54
+ <button
55
+ key={preset}
56
+ onClick={() => handlePresetClick(preset)}
57
+ className={`${styles.presetButton} ${timeRange.preset === preset ? styles.active : ''}`}
58
+ >
59
+ {label}
60
+ </button>
61
+ ))}
62
+ </div>
63
+
64
+ {timeRange.preset === 'custom' && (
65
+ <div className={styles.customRange}>
66
+ <div className={styles.dateInputGroup}>
67
+ <label
68
+ htmlFor="start-time"
69
+ className={styles.dateLabel}
70
+ >
71
+ Start
72
+ </label>
73
+ <input
74
+ id="start-time"
75
+ type="datetime-local"
76
+ value={
77
+ timeRange.start ? formatDateTimeLocal(timeRange.start) : ''
78
+ }
79
+ onChange={handleStartChange}
80
+ className={styles.dateInput}
81
+ />
82
+ </div>
83
+
84
+ <div className={styles.dateInputGroup}>
85
+ <label
86
+ htmlFor="end-time"
87
+ className={styles.dateLabel}
88
+ >
89
+ End
90
+ </label>
91
+ <input
92
+ id="end-time"
93
+ type="datetime-local"
94
+ value={timeRange.end ? formatDateTimeLocal(timeRange.end) : ''}
95
+ onChange={handleEndChange}
96
+ className={styles.dateInput}
97
+ />
98
+ </div>
99
+ </div>
100
+ )}
101
+ </div>
102
+ );
103
+ };
104
+
105
+ export default TimeRangeFilter;
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { ReactFlowProvider } from '@xyflow/react';
4
+ import AgentNode from './AgentNode';
5
+ import { AgentNodeData } from '../../types/graph';
6
+ import { NodeProps } from '@xyflow/react';
7
+
8
+ describe('AgentNode', () => {
9
+ const createNodeProps = (data: AgentNodeData, selected = false): NodeProps =>
10
+ ({
11
+ id: 'test-agent',
12
+ data,
13
+ selected,
14
+ type: 'agent',
15
+ isConnectable: true,
16
+ dragging: false,
17
+ zIndex: 0,
18
+ selectable: true,
19
+ deletable: true,
20
+ draggable: true,
21
+ }) as unknown as NodeProps;
22
+
23
+ it('should render agent name', () => {
24
+ const data: AgentNodeData = {
25
+ name: 'test-agent',
26
+ status: 'idle',
27
+ subscriptions: ['Movie'],
28
+ sentCount: 5,
29
+ recvCount: 3,
30
+ };
31
+
32
+ render(
33
+ <ReactFlowProvider>
34
+ <AgentNode {...createNodeProps(data)} />
35
+ </ReactFlowProvider>
36
+ );
37
+ expect(screen.getByText('test-agent')).toBeInTheDocument();
38
+ });
39
+
40
+ it('should render subscriptions', () => {
41
+ const data: AgentNodeData = {
42
+ name: 'test-agent',
43
+ status: 'idle',
44
+ subscriptions: ['Movie', 'Tagline'],
45
+ sentCount: 5,
46
+ recvCount: 3,
47
+ };
48
+
49
+ render(
50
+ <ReactFlowProvider>
51
+ <AgentNode {...createNodeProps(data)} />
52
+ </ReactFlowProvider>
53
+ );
54
+ expect(screen.getByText('Movie')).toBeInTheDocument();
55
+ expect(screen.getByText('Tagline')).toBeInTheDocument();
56
+ });
57
+
58
+ it('should render sent and received counts', () => {
59
+ const data: AgentNodeData = {
60
+ name: 'test-agent',
61
+ status: 'idle',
62
+ subscriptions: [],
63
+ sentCount: 5,
64
+ recvCount: 3,
65
+ };
66
+
67
+ render(
68
+ <ReactFlowProvider>
69
+ <AgentNode {...createNodeProps(data)} />
70
+ </ReactFlowProvider>
71
+ );
72
+ expect(screen.getByText(/↑ 5/)).toBeInTheDocument();
73
+ expect(screen.getByText(/↓ 3/)).toBeInTheDocument();
74
+ });
75
+ });