@parca/profile 0.19.139 → 0.19.142

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.
Files changed (170) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/GraphTooltipArrow/Content.js +224 -30
  3. package/dist/GraphTooltipArrow/DockedGraphTooltip/index.js +192 -33
  4. package/dist/GraphTooltipArrow/ExpandOnHoverValue.js +53 -3
  5. package/dist/GraphTooltipArrow/index.d.ts.map +1 -1
  6. package/dist/GraphTooltipArrow/index.js +86 -56
  7. package/dist/GraphTooltipArrow/useGraphTooltip/index.js +37 -37
  8. package/dist/GraphTooltipArrow/useGraphTooltipMetaInfo/index.js +103 -73
  9. package/dist/MatchersInput/SuggestionItem.js +91 -12
  10. package/dist/MatchersInput/SuggestionsList.d.ts +2 -1
  11. package/dist/MatchersInput/SuggestionsList.d.ts.map +1 -1
  12. package/dist/MatchersInput/SuggestionsList.js +371 -157
  13. package/dist/MatchersInput/SuggestionsList.test.d.ts +2 -0
  14. package/dist/MatchersInput/SuggestionsList.test.d.ts.map +1 -0
  15. package/dist/MatchersInput/index.js +308 -115
  16. package/dist/MetricsCircle/index.js +39 -3
  17. package/dist/MetricsGraph/MetricsContextMenu/index.js +119 -19
  18. package/dist/MetricsGraph/MetricsInfoPanel/index.js +81 -20
  19. package/dist/MetricsGraph/MetricsTooltip/index.d.ts.map +1 -1
  20. package/dist/MetricsGraph/MetricsTooltip/index.js +107 -74
  21. package/dist/MetricsGraph/index.js +552 -203
  22. package/dist/MetricsGraph/useMetricsGraphDimensions.js +46 -25
  23. package/dist/MetricsGraph/utils/colorMapping.js +24 -17
  24. package/dist/MetricsSeries/index.js +70 -7
  25. package/dist/PreSelectedMatchers/index.d.ts.map +1 -1
  26. package/dist/PreSelectedMatchers/index.js +249 -102
  27. package/dist/ProfileExplorer/ProfileExplorerCompare.js +240 -49
  28. package/dist/ProfileExplorer/ProfileExplorerSingle.js +98 -11
  29. package/dist/ProfileExplorer/index.js +183 -32
  30. package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.js +333 -148
  31. package/dist/ProfileFlameChart/SamplesStrips/SamplesStrips.stories.js +69 -35
  32. package/dist/ProfileFlameChart/SamplesStrips/index.js +645 -134
  33. package/dist/ProfileFlameChart/SamplesStrips/labelSetUtils.js +114 -55
  34. package/dist/ProfileFlameChart/index.js +266 -134
  35. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.js +287 -88
  36. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenuWrapper.js +56 -20
  37. package/dist/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.js +211 -140
  38. package/dist/ProfileFlameGraph/FlameGraphArrow/MemoizedTooltip.js +133 -38
  39. package/dist/ProfileFlameGraph/FlameGraphArrow/MiniMap.js +261 -216
  40. package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.d.ts.map +1 -1
  41. package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.js +71 -45
  42. package/dist/ProfileFlameGraph/FlameGraphArrow/TooltipContext.d.ts.map +1 -1
  43. package/dist/ProfileFlameGraph/FlameGraphArrow/TooltipContext.js +58 -28
  44. package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.d.ts.map +1 -1
  45. package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.js +59 -8
  46. package/dist/ProfileFlameGraph/FlameGraphArrow/index.js +396 -179
  47. package/dist/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.d.ts.map +1 -1
  48. package/dist/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.js +68 -50
  49. package/dist/ProfileFlameGraph/FlameGraphArrow/useMappingList.js +62 -38
  50. package/dist/ProfileFlameGraph/FlameGraphArrow/useNodeColor.js +14 -6
  51. package/dist/ProfileFlameGraph/FlameGraphArrow/useScrollViewport.js +124 -82
  52. package/dist/ProfileFlameGraph/FlameGraphArrow/useVisibleNodes.js +160 -98
  53. package/dist/ProfileFlameGraph/FlameGraphArrow/useZoom.js +232 -112
  54. package/dist/ProfileFlameGraph/FlameGraphArrow/utils.js +137 -114
  55. package/dist/ProfileFlameGraph/benchmarks/benchdata/populateData.js +85 -0
  56. package/dist/ProfileFlameGraph/index.js +324 -148
  57. package/dist/ProfileMetricsGraph/hooks/useQueryRange.js +140 -32
  58. package/dist/ProfileMetricsGraph/index.js +518 -259
  59. package/dist/ProfileSelector/CompareButton.js +132 -12
  60. package/dist/ProfileSelector/MetricsGraphSection.js +234 -67
  61. package/dist/ProfileSelector/index.d.ts.map +1 -1
  62. package/dist/ProfileSelector/index.js +730 -142
  63. package/dist/ProfileSelector/useAutoQuerySelector.js +249 -130
  64. package/dist/ProfileSource.js +230 -163
  65. package/dist/ProfileTypeSelector/index.js +214 -125
  66. package/dist/ProfileView/components/ActionButtons/GroupByDropdown.js +50 -4
  67. package/dist/ProfileView/components/ActionButtons/SortByDropdown.js +139 -33
  68. package/dist/ProfileView/components/ColorStackLegend.js +184 -55
  69. package/dist/ProfileView/components/DashboardItems/index.js +87 -28
  70. package/dist/ProfileView/components/DashboardLayout/index.js +108 -16
  71. package/dist/ProfileView/components/DiffLegend.js +172 -29
  72. package/dist/ProfileView/components/GroupByLabelsDropdown/index.js +199 -55
  73. package/dist/ProfileView/components/InvertCallStack/index.js +99 -10
  74. package/dist/ProfileView/components/ProfileFilters/filterPresets.js +260 -315
  75. package/dist/ProfileView/components/ProfileFilters/index.js +518 -215
  76. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.js +370 -306
  77. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +188 -120
  78. package/dist/ProfileView/components/ProfileHeader/index.js +105 -11
  79. package/dist/ProfileView/components/ShareButton/ResultBox.js +119 -16
  80. package/dist/ProfileView/components/ShareButton/index.js +352 -62
  81. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts.map +1 -1
  82. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +675 -195
  83. package/dist/ProfileView/components/Toolbars/SwitchMenuItem.js +94 -7
  84. package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.js +198 -157
  85. package/dist/ProfileView/components/Toolbars/index.js +441 -21
  86. package/dist/ProfileView/components/ViewSelector/Dropdown.js +233 -22
  87. package/dist/ProfileView/components/ViewSelector/index.js +211 -91
  88. package/dist/ProfileView/components/VisualizationContainer/index.d.ts.map +1 -1
  89. package/dist/ProfileView/components/VisualizationContainer/index.js +52 -7
  90. package/dist/ProfileView/components/VisualizationPanel.js +185 -8
  91. package/dist/ProfileView/context/DashboardContext.js +84 -28
  92. package/dist/ProfileView/context/ProfileViewContext.js +56 -15
  93. package/dist/ProfileView/hooks/useAutoSelectDimension.js +71 -41
  94. package/dist/ProfileView/hooks/useProfileMetadata.js +50 -18
  95. package/dist/ProfileView/hooks/useResetFlameGraphState.js +31 -10
  96. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.js +72 -29
  97. package/dist/ProfileView/hooks/useResetStateOnSeriesChange.js +39 -13
  98. package/dist/ProfileView/hooks/useVisualizationState.js +262 -87
  99. package/dist/ProfileView/index.js +383 -45
  100. package/dist/ProfileView/types/visualization.js +1 -13
  101. package/dist/ProfileView/utils/colorUtils.js +8 -7
  102. package/dist/ProfileViewWithData.js +332 -237
  103. package/dist/QueryControls/index.js +418 -47
  104. package/dist/Sandwich/components/CalleesSection.js +54 -4
  105. package/dist/Sandwich/components/CallersSection.js +97 -27
  106. package/dist/Sandwich/components/TableSection.js +77 -4
  107. package/dist/Sandwich/index.js +125 -12
  108. package/dist/Sandwich/utils/processRowData.js +48 -39
  109. package/dist/SelectWithRefresh/index.js +102 -28
  110. package/dist/SimpleMatchers/Select.js +520 -187
  111. package/dist/SimpleMatchers/index.js +590 -288
  112. package/dist/SourceView/Highlighter.js +230 -70
  113. package/dist/SourceView/LineNo.js +72 -17
  114. package/dist/SourceView/index.js +177 -101
  115. package/dist/SourceView/lang-detector/ext-to-lang.json +798 -798
  116. package/dist/SourceView/lang-detector/index.js +28 -14
  117. package/dist/SourceView/useSelectedLineRange.js +97 -16
  118. package/dist/Table/ColorCell.js +42 -1
  119. package/dist/Table/ColumnsVisibility.js +114 -6
  120. package/dist/Table/MoreDropdown.js +121 -27
  121. package/dist/Table/TableContextMenu.js +150 -139
  122. package/dist/Table/TableContextMenuWrapper.js +59 -14
  123. package/dist/Table/hooks/useColorManagement.js +58 -16
  124. package/dist/Table/hooks/useTableConfiguration.d.ts.map +1 -1
  125. package/dist/Table/hooks/useTableConfiguration.js +331 -168
  126. package/dist/Table/index.js +222 -126
  127. package/dist/Table/utils/functions.js +169 -144
  128. package/dist/Table/utils/topAndBottomExpandedRowModel.js +69 -52
  129. package/dist/TimelineGuide/index.js +209 -16
  130. package/dist/TopTable/benchmarks/benchdata/populateData.js +91 -0
  131. package/dist/TopTable/index.js +340 -122
  132. package/dist/contexts/LabelsQueryProvider.js +94 -32
  133. package/dist/contexts/UnifiedLabelsContext.js +114 -49
  134. package/dist/contexts/utils.js +37 -15
  135. package/dist/hooks/useCompareModeMeta.js +157 -94
  136. package/dist/hooks/useLabels.js +295 -52
  137. package/dist/hooks/useQueryState.js +371 -330
  138. package/dist/index.js +21 -16
  139. package/dist/testdata/fg-diff.json +3750 -0
  140. package/dist/testdata/fg-simple.json +1879 -0
  141. package/dist/testdata/link_data.json +56 -0
  142. package/dist/testdata/tabular.json +30 -0
  143. package/dist/testdata/test_flamegraph.json +26846 -0
  144. package/dist/testdata/test_graph.json +53 -0
  145. package/dist/useDelayedLoader.js +32 -18
  146. package/dist/useGrpcQuery/index.js +71 -11
  147. package/dist/useHasProfileData.js +90 -12
  148. package/dist/useQuery.js +205 -64
  149. package/dist/useSumBy.d.ts.map +1 -1
  150. package/dist/useSumBy.js +294 -138
  151. package/dist/utils.js +62 -30
  152. package/package.json +9 -9
  153. package/src/GraphTooltipArrow/index.tsx +3 -0
  154. package/src/MatchersInput/SuggestionsList.test.tsx +70 -0
  155. package/src/MatchersInput/SuggestionsList.tsx +11 -10
  156. package/src/MatchersInput/index.tsx +1 -1
  157. package/src/MetricsGraph/MetricsTooltip/index.tsx +22 -34
  158. package/src/PreSelectedMatchers/index.tsx +3 -0
  159. package/src/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.tsx +3 -0
  160. package/src/ProfileFlameGraph/FlameGraphArrow/TooltipContext.tsx +3 -0
  161. package/src/ProfileFlameGraph/FlameGraphArrow/ZoomControls.tsx +3 -0
  162. package/src/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.ts +3 -0
  163. package/src/ProfileSelector/index.tsx +30 -7
  164. package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +3 -0
  165. package/src/ProfileView/components/VisualizationContainer/index.tsx +3 -0
  166. package/src/Table/hooks/useTableConfiguration.tsx +7 -13
  167. package/src/useDelayedLoader.ts +10 -10
  168. package/src/useSumBy.ts +12 -18
  169. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.js +0 -541
  170. package/dist/hooks/useQueryState.test.js +0 -984
@@ -1,984 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- // Copyright 2022 The Parca Authors
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
- import { act } from 'react';
15
- // eslint-disable-next-line import/named
16
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
17
- // eslint-disable-next-line import/named
18
- import { renderHook, waitFor } from '@testing-library/react';
19
- import { beforeEach, describe, expect, it, vi } from 'vitest';
20
- import { URLStateProvider } from '@parca/components';
21
- import { useQueryState } from './useQueryState';
22
- // Mock window.location
23
- const mockLocation = {
24
- pathname: '/test',
25
- search: '',
26
- };
27
- // Mock the navigate function that actually updates the mock location
28
- const mockNavigateTo = vi.fn((path, params) => {
29
- // Convert params object to query string
30
- const searchParams = new URLSearchParams();
31
- Object.entries(params).forEach(([key, value]) => {
32
- if (value !== undefined && value !== null) {
33
- if (Array.isArray(value)) {
34
- // For arrays, join with commas
35
- searchParams.set(key, value.join(','));
36
- }
37
- else {
38
- searchParams.set(key, String(value));
39
- }
40
- }
41
- });
42
- mockLocation.search = `?${searchParams.toString()}`;
43
- });
44
- // Mock the getQueryParamsFromURL function
45
- vi.mock('@parca/components/src/hooks/URLState/utils', async () => {
46
- const actual = await vi.importActual('@parca/components/src/hooks/URLState/utils');
47
- return {
48
- ...actual,
49
- getQueryParamsFromURL: () => {
50
- if (mockLocation.search === '')
51
- return {};
52
- const params = new URLSearchParams(mockLocation.search);
53
- const result = {};
54
- for (const [key, value] of params.entries()) {
55
- const decodedValue = decodeURIComponent(value);
56
- const existing = result[key];
57
- if (existing !== undefined) {
58
- result[key] = Array.isArray(existing)
59
- ? [...existing, decodedValue]
60
- : [existing, decodedValue];
61
- }
62
- else {
63
- result[key] = decodedValue;
64
- }
65
- }
66
- return result;
67
- },
68
- };
69
- });
70
- // Mock useSumBy with stateful behavior using React's useState
71
- vi.mock('../useSumBy', async () => {
72
- const actual = await vi.importActual('../useSumBy');
73
- const react = await import('react');
74
- return {
75
- ...actual,
76
- useSumBy: (_queryClient, _profileType, _timeRange, _draftProfileType, _draftTimeRange, defaultValue) => {
77
- const [draftSumBy, setDraftSumBy] = react.useState(defaultValue);
78
- const [sumBy, setSumBy] = react.useState(defaultValue);
79
- return {
80
- sumBy,
81
- setSumBy,
82
- isLoading: false,
83
- draftSumBy,
84
- setDraftSumBy,
85
- isDraftSumByLoading: false,
86
- };
87
- },
88
- };
89
- });
90
- // Track profile types loading state for tests
91
- let mockProfileTypesLoading = false;
92
- let mockProfileTypesData;
93
- // Mock useProfileTypes to control loading state in tests
94
- vi.mock('../ProfileSelector', async () => {
95
- const actual = await vi.importActual('../ProfileSelector');
96
- return {
97
- ...actual,
98
- useProfileTypes: () => ({
99
- loading: mockProfileTypesLoading,
100
- data: mockProfileTypesData,
101
- error: null,
102
- }),
103
- };
104
- });
105
- // Helper to set profile types loading state for tests
106
- const setProfileTypesLoading = (loading) => {
107
- mockProfileTypesLoading = loading;
108
- };
109
- const setProfileTypesData = (data) => {
110
- mockProfileTypesData = data;
111
- };
112
- // Helper to create wrapper with URLStateProvider
113
- const createWrapper = (paramPreferences = {}) => {
114
- const queryClient = new QueryClient({
115
- defaultOptions: {
116
- queries: {
117
- retry: false,
118
- },
119
- },
120
- });
121
- const Wrapper = ({ children }) => (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(URLStateProvider, { navigateTo: mockNavigateTo, paramPreferences: paramPreferences, children: children }) }));
122
- Wrapper.displayName = 'URLStateProviderWrapper';
123
- return Wrapper;
124
- };
125
- describe('useQueryState', () => {
126
- beforeEach(() => {
127
- mockNavigateTo.mockClear();
128
- Object.defineProperty(window, 'location', {
129
- value: mockLocation,
130
- writable: true,
131
- });
132
- mockLocation.search = '';
133
- // Reset profile types mock state
134
- setProfileTypesLoading(false);
135
- setProfileTypesData(undefined);
136
- });
137
- describe('Basic functionality', () => {
138
- it('should initialize with default values', () => {
139
- const { result } = renderHook(() => useQueryState({
140
- defaultExpression: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{}',
141
- defaultTimeSelection: 'relative:hour|1',
142
- defaultFrom: 1000,
143
- defaultTo: 2000,
144
- }), { wrapper: createWrapper() });
145
- const { querySelection } = result.current;
146
- expect(querySelection.expression).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{}');
147
- expect(querySelection.timeSelection).toBe('relative:hour|1');
148
- // From/to should be calculated from the range
149
- expect(querySelection.from).toBeDefined();
150
- expect(querySelection.to).toBeDefined();
151
- });
152
- it('should handle suffix for comparison mode', () => {
153
- mockLocation.search =
154
- '?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from_a=1000&to_a=2000';
155
- const { result } = renderHook(() => useQueryState({ suffix: '_a' }), { wrapper: createWrapper() });
156
- const { querySelection } = result.current;
157
- expect(querySelection.expression).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}');
158
- expect(querySelection.from).toBe(1000);
159
- expect(querySelection.to).toBe(2000);
160
- });
161
- });
162
- describe('Individual setters', () => {
163
- it('should update expression and handle delta profiles', async () => {
164
- const { result } = renderHook(() => useQueryState({
165
- defaultFrom: 1000,
166
- defaultTo: 2000,
167
- }), { wrapper: createWrapper() });
168
- act(() => {
169
- result.current.setDraftExpression('memory:alloc_objects:count:space:bytes:delta{}');
170
- });
171
- // Draft should be updated but not committed
172
- expect(result.current.draftSelection.expression).toBe('memory:alloc_objects:count:space:bytes:delta{}');
173
- // Delta profile should auto-calculate merge params in draft
174
- expect(result.current.draftSelection.mergeFrom).toBe('1000000000');
175
- expect(result.current.draftSelection.mergeTo).toBe('2000000000');
176
- act(() => {
177
- result.current.commitDraft();
178
- });
179
- await waitFor(() => {
180
- expect(mockNavigateTo).toHaveBeenCalled();
181
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
182
- expect(params.expression).toBe('memory:alloc_objects:count:space:bytes:delta{}');
183
- // Should set merge parameters for delta profile
184
- expect(params).toHaveProperty('merge_from');
185
- expect(params).toHaveProperty('merge_to');
186
- expect(params.merge_from).toBe('1000000000');
187
- expect(params.merge_to).toBe('2000000000');
188
- });
189
- });
190
- it('should update time range', async () => {
191
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
192
- act(() => {
193
- result.current.setDraftTimeRange(3000, 4000, 'relative:minute|5');
194
- });
195
- // Draft should be updated
196
- expect(result.current.draftSelection.from).toBe(3000);
197
- expect(result.current.draftSelection.to).toBe(4000);
198
- expect(result.current.draftSelection.timeSelection).toBe('relative:minute|5');
199
- act(() => {
200
- result.current.commitDraft();
201
- });
202
- await waitFor(() => {
203
- expect(mockNavigateTo).toHaveBeenCalled();
204
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
205
- expect(params.from).toBe('3000');
206
- expect(params.to).toBe('4000');
207
- expect(params.time_selection).toBe('relative:minute|5');
208
- });
209
- });
210
- it('should update sumBy', async () => {
211
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
212
- // sumBy only applies to delta profiles, so we need to set one first
213
- act(() => {
214
- result.current.setDraftExpression('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}');
215
- result.current.setDraftSumBy(['namespace', 'container']);
216
- });
217
- // Draft should be updated
218
- expect(result.current.draftSelection.sumBy).toEqual(['namespace', 'container']);
219
- act(() => {
220
- result.current.commitDraft();
221
- });
222
- await waitFor(() => {
223
- expect(mockNavigateTo).toHaveBeenCalled();
224
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
225
- expect(params.sum_by).toBe('namespace,container');
226
- });
227
- });
228
- it('should auto-calculate merge range for delta profiles', async () => {
229
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
230
- // Set a delta profile expression
231
- act(() => {
232
- result.current.setDraftExpression('memory:alloc_space:bytes:space:bytes:delta{}');
233
- result.current.setDraftTimeRange(5000, 6000, 'relative:minute|5');
234
- });
235
- // Merge range should be auto-calculated in draft
236
- expect(result.current.draftSelection.mergeFrom).toBe('5000000000');
237
- expect(result.current.draftSelection.mergeTo).toBe('6000000000');
238
- act(() => {
239
- result.current.commitDraft();
240
- });
241
- await waitFor(() => {
242
- expect(mockNavigateTo).toHaveBeenCalled();
243
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
244
- expect(params.merge_from).toBe('5000000000');
245
- expect(params.merge_to).toBe('6000000000');
246
- });
247
- });
248
- });
249
- describe('Batch updates', () => {
250
- it('should batch multiple updates into single navigation', async () => {
251
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
252
- act(() => {
253
- // Update multiple draft values (using delta profile since sumBy only applies to delta)
254
- result.current.setDraftExpression('memory:alloc_space:bytes:space:bytes:delta{}');
255
- result.current.setDraftTimeRange(7000, 8000, 'relative:minute|30');
256
- result.current.setDraftSumBy(['pod', 'node']);
257
- });
258
- // All drafts should be updated
259
- expect(result.current.draftSelection.expression).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
260
- expect(result.current.draftSelection.from).toBe(7000);
261
- expect(result.current.draftSelection.to).toBe(8000);
262
- expect(result.current.draftSelection.sumBy).toEqual(['pod', 'node']);
263
- act(() => {
264
- result.current.commitDraft();
265
- });
266
- await waitFor(() => {
267
- // Should only navigate once for all updates
268
- expect(mockNavigateTo).toHaveBeenCalledTimes(1);
269
- const [, params] = mockNavigateTo.mock.calls[0];
270
- expect(params.expression).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
271
- expect(params.from).toBe('7000');
272
- expect(params.to).toBe('8000');
273
- expect(params.time_selection).toBe('relative:minute|30');
274
- expect(params.sum_by).toBe('pod,node');
275
- });
276
- });
277
- it('should handle partial updates', async () => {
278
- mockLocation.search =
279
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from=1000&to=2000&time_selection=relative:hour|1';
280
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
281
- act(() => {
282
- // Only update expression, other values should remain
283
- result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
284
- });
285
- expect(result.current.draftSelection.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
286
- // Other values should be from URL
287
- expect(result.current.draftSelection.from).toBe(1000);
288
- expect(result.current.draftSelection.to).toBe(2000);
289
- act(() => {
290
- result.current.commitDraft();
291
- });
292
- await waitFor(() => {
293
- expect(mockNavigateTo).toHaveBeenCalled();
294
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
295
- expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
296
- expect(params.from).toBe('1000');
297
- expect(params.to).toBe('2000');
298
- expect(params.time_selection).toBe('relative:hour|1');
299
- });
300
- });
301
- it('should auto-calculate merge params for delta profiles in batch update', async () => {
302
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
303
- act(() => {
304
- result.current.setDraftExpression('memory:alloc_space:bytes:space:bytes:delta{}');
305
- result.current.setDraftTimeRange(9000, 10000, 'relative:minute|5');
306
- });
307
- // Merge params should be auto-calculated in draft
308
- expect(result.current.draftSelection.mergeFrom).toBe('9000000000');
309
- expect(result.current.draftSelection.mergeTo).toBe('10000000000');
310
- act(() => {
311
- result.current.commitDraft();
312
- });
313
- await waitFor(() => {
314
- expect(mockNavigateTo).toHaveBeenCalled();
315
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
316
- expect(params.expression).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
317
- expect(params.merge_from).toBe('9000000000');
318
- expect(params.merge_to).toBe('10000000000');
319
- });
320
- });
321
- });
322
- describe('Helper functions', () => {
323
- it('should set profile name correctly', async () => {
324
- mockLocation.search =
325
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{job="parca"}';
326
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
327
- act(() => {
328
- result.current.setDraftProfileName('memory:inuse_space:bytes:space:bytes');
329
- });
330
- // Draft should be updated
331
- expect(result.current.draftSelection.expression).toBe('memory:inuse_space:bytes:space:bytes{job="parca"}');
332
- act(() => {
333
- result.current.commitDraft();
334
- });
335
- await waitFor(() => {
336
- expect(mockNavigateTo).toHaveBeenCalled();
337
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
338
- expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{job="parca"}');
339
- });
340
- });
341
- it('should set matchers correctly using draft', async () => {
342
- mockLocation.search = '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}';
343
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
344
- act(() => {
345
- result.current.setDraftMatchers('namespace="default",pod="my-pod"');
346
- });
347
- // Draft should be updated but not URL yet
348
- expect(result.current.draftSelection.expression).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{namespace="default",pod="my-pod"}');
349
- expect(mockNavigateTo).not.toHaveBeenCalled();
350
- // Commit the draft
351
- act(() => {
352
- result.current.commitDraft();
353
- });
354
- await waitFor(() => {
355
- expect(mockNavigateTo).toHaveBeenCalled();
356
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
357
- expect(params.expression).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{namespace="default",pod="my-pod"}');
358
- });
359
- });
360
- });
361
- describe('Comparison mode', () => {
362
- it('should handle _a suffix correctly', async () => {
363
- const { result } = renderHook(() => useQueryState({ suffix: '_a' }), { wrapper: createWrapper() });
364
- // Update draft state
365
- act(() => {
366
- result.current.setDraftExpression('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}');
367
- result.current.setDraftTimeRange(1111, 2222, 'relative:hour|1');
368
- result.current.setDraftSumBy(['label_a']);
369
- });
370
- // Commit draft
371
- act(() => {
372
- result.current.commitDraft();
373
- });
374
- await waitFor(() => {
375
- expect(mockNavigateTo).toHaveBeenCalled();
376
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
377
- expect(params.expression_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}');
378
- expect(params.from_a).toBe('1111');
379
- expect(params.to_a).toBe('2222');
380
- expect(params.sum_by_a).toBe('label_a');
381
- });
382
- });
383
- it('should handle _b suffix correctly', async () => {
384
- const { result } = renderHook(() => useQueryState({ suffix: '_b' }), { wrapper: createWrapper() });
385
- // Update draft state (using delta profile since sumBy only applies to delta)
386
- act(() => {
387
- result.current.setDraftExpression('memory:alloc_space:bytes:space:bytes:delta{}');
388
- result.current.setDraftTimeRange(3333, 4444, 'relative:hour|2');
389
- result.current.setDraftSumBy(['label_b']);
390
- });
391
- // Commit draft
392
- act(() => {
393
- result.current.commitDraft();
394
- });
395
- await waitFor(() => {
396
- expect(mockNavigateTo).toHaveBeenCalled();
397
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
398
- expect(params.expression_b).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
399
- expect(params.from_b).toBe('3333');
400
- expect(params.to_b).toBe('4444');
401
- expect(params.sum_by_b).toBe('label_b');
402
- });
403
- });
404
- });
405
- describe('Draft state pattern', () => {
406
- it('should not update URL until commit', async () => {
407
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
408
- // Make multiple draft changes (using delta profile since sumBy only applies to delta)
409
- act(() => {
410
- result.current.setDraftExpression('memory:alloc_space:bytes:space:bytes:delta{}');
411
- result.current.setDraftTimeRange(5000, 6000, 'relative:hour|3');
412
- result.current.setDraftSumBy(['namespace', 'pod']);
413
- });
414
- // URL should not be updated yet
415
- expect(mockNavigateTo).not.toHaveBeenCalled();
416
- // Commit all changes at once
417
- act(() => {
418
- result.current.commitDraft();
419
- });
420
- // Now URL should be updated exactly once with all changes
421
- await waitFor(() => {
422
- expect(mockNavigateTo).toHaveBeenCalledTimes(1);
423
- const [, params] = mockNavigateTo.mock.calls[0];
424
- expect(params.expression).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
425
- expect(params.from).toBe('5000');
426
- expect(params.to).toBe('6000');
427
- expect(params.sum_by).toBe('namespace,pod');
428
- });
429
- });
430
- it('should handle draft profile name changes', () => {
431
- mockLocation.search =
432
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{job="test"}';
433
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
434
- // Change profile name in draft
435
- act(() => {
436
- result.current.setDraftProfileName('memory:inuse_space:bytes:space:bytes');
437
- });
438
- // Draft should be updated
439
- expect(result.current.draftSelection.expression).toBe('memory:inuse_space:bytes:space:bytes{job="test"}');
440
- // URL should not be updated yet
441
- expect(mockNavigateTo).not.toHaveBeenCalled();
442
- });
443
- });
444
- describe('Edge cases', () => {
445
- it('should handle invalid expression gracefully and log warning', () => {
446
- const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
447
- const { result } = renderHook(() => useQueryState({
448
- defaultExpression: 'invalid{{}expression',
449
- }), { wrapper: createWrapper() });
450
- // Should not throw error - invalid expressions are caught and logged
451
- expect(() => result.current.querySelection).not.toThrow();
452
- // Should fall back to empty expression
453
- expect(result.current.querySelection.expression).toBe('invalid{{}expression');
454
- // Should log a warning about the parse failure
455
- expect(consoleSpy).toHaveBeenCalledWith('Failed to parse expression', expect.objectContaining({
456
- expression: 'invalid{{}expression',
457
- }));
458
- consoleSpy.mockRestore();
459
- });
460
- it('should handle empty expression gracefully', () => {
461
- const { result } = renderHook(() => useQueryState({
462
- defaultExpression: '',
463
- }), { wrapper: createWrapper() });
464
- // Should not throw error with empty expression
465
- expect(() => result.current.querySelection).not.toThrow();
466
- expect(result.current.querySelection.expression).toBe('');
467
- });
468
- it('should clear merge params for non-delta profiles', async () => {
469
- mockLocation.search =
470
- '?expression=memory:alloc_objects:count:space:bytes:delta{}&merge_from=1000000000&merge_to=2000000000';
471
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
472
- // Switch to non-delta profile (without :delta suffix) using draft
473
- act(() => {
474
- result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
475
- });
476
- // Commit the draft
477
- act(() => {
478
- result.current.commitDraft();
479
- });
480
- await waitFor(() => {
481
- expect(mockNavigateTo).toHaveBeenCalled();
482
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
483
- expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
484
- expect(params).not.toHaveProperty('merge_from');
485
- expect(params).not.toHaveProperty('merge_to');
486
- });
487
- });
488
- it('should preserve other URL parameters when updating', async () => {
489
- mockLocation.search =
490
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&other_param=value&unrelated=test';
491
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
492
- // Update draft and commit
493
- act(() => {
494
- result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
495
- });
496
- act(() => {
497
- result.current.commitDraft();
498
- });
499
- await waitFor(() => {
500
- expect(mockNavigateTo).toHaveBeenCalled();
501
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
502
- expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
503
- expect(params.other_param).toBe('value');
504
- expect(params.unrelated).toBe('test');
505
- });
506
- });
507
- });
508
- describe('Commit with refreshed time range (time range re-evaluation)', () => {
509
- it('should use refreshed time range values instead of draft state when provided', async () => {
510
- mockLocation.search =
511
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&from=1000&to=2000&time_selection=relative:minute|15';
512
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
513
- // Draft state has original values
514
- expect(result.current.draftSelection.from).toBe(1000);
515
- expect(result.current.draftSelection.to).toBe(2000);
516
- expect(result.current.draftSelection.timeSelection).toBe('relative:minute|15');
517
- // Commit with refreshed time range (simulating re-evaluated time range)
518
- act(() => {
519
- result.current.commitDraft({
520
- from: 5000,
521
- to: 6000,
522
- timeSelection: 'relative:minute|15',
523
- });
524
- });
525
- await waitFor(() => {
526
- expect(mockNavigateTo).toHaveBeenCalled();
527
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
528
- // Should use refreshed time range values, not draft values
529
- expect(params.from).toBe('5000');
530
- expect(params.to).toBe('6000');
531
- expect(params.time_selection).toBe('relative:minute|15');
532
- });
533
- });
534
- it('should update draft state with refreshed time range after commit', async () => {
535
- const { result } = renderHook(() => useQueryState({
536
- defaultExpression: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}',
537
- defaultFrom: 1000,
538
- defaultTo: 2000,
539
- defaultTimeSelection: 'relative:minute|5',
540
- }), { wrapper: createWrapper() });
541
- // Commit with refreshed time values
542
- act(() => {
543
- result.current.commitDraft({
544
- from: 3000,
545
- to: 4000,
546
- timeSelection: 'relative:minute|5',
547
- });
548
- });
549
- await waitFor(() => {
550
- expect(mockNavigateTo).toHaveBeenCalled();
551
- });
552
- // Draft state should be updated with the refreshed time range
553
- expect(result.current.draftSelection.from).toBe(3000);
554
- expect(result.current.draftSelection.to).toBe(4000);
555
- });
556
- it('should trigger navigation even when expression unchanged (time re-evaluation)', async () => {
557
- mockLocation.search =
558
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&from=1000&to=2000&time_selection=relative:minute|5';
559
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
560
- mockNavigateTo.mockClear();
561
- // First commit with new time values
562
- act(() => {
563
- result.current.commitDraft({
564
- from: 5000,
565
- to: 6000,
566
- timeSelection: 'relative:minute|5',
567
- });
568
- });
569
- await waitFor(() => {
570
- expect(mockNavigateTo).toHaveBeenCalledTimes(1);
571
- });
572
- const firstCallParams = mockNavigateTo.mock.calls[0][1];
573
- expect(firstCallParams.from).toBe('5000');
574
- expect(firstCallParams.to).toBe('6000');
575
- mockNavigateTo.mockClear();
576
- // Second commit with different time values (simulating clicking Search again)
577
- act(() => {
578
- result.current.commitDraft({
579
- from: 7000,
580
- to: 8000,
581
- timeSelection: 'relative:minute|5',
582
- });
583
- });
584
- await waitFor(() => {
585
- expect(mockNavigateTo).toHaveBeenCalledTimes(1);
586
- });
587
- const secondCallParams = mockNavigateTo.mock.calls[0][1];
588
- expect(secondCallParams.from).toBe('7000');
589
- expect(secondCallParams.to).toBe('8000');
590
- // Verify that navigation was called both times despite expression being unchanged
591
- expect(firstCallParams.from).not.toBe(secondCallParams.from);
592
- });
593
- it('should auto-calculate merge params for delta profiles when using refreshed time range', async () => {
594
- const { result } = renderHook(() => useQueryState({
595
- defaultExpression: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}',
596
- defaultFrom: 1000,
597
- defaultTo: 2000,
598
- }), { wrapper: createWrapper() });
599
- // Commit with refreshed time range for delta profile
600
- act(() => {
601
- result.current.commitDraft({
602
- from: 5000,
603
- to: 6000,
604
- timeSelection: 'relative:minute|5',
605
- });
606
- });
607
- await waitFor(() => {
608
- expect(mockNavigateTo).toHaveBeenCalled();
609
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
610
- // Verify merge params are calculated from refreshed time range
611
- expect(params.merge_from).toBe('5000000000'); // 5000ms * 1_000_000
612
- expect(params.merge_to).toBe('6000000000'); // 6000ms * 1_000_000
613
- });
614
- });
615
- it('should use draft values when refreshedTimeRange is not provided', async () => {
616
- mockLocation.search =
617
- '?expression=memory:inuse_space:bytes:space:bytes{}&from=1000&to=2000&time_selection=relative:hour|1';
618
- const { result } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
619
- // Change draft values
620
- act(() => {
621
- result.current.setDraftTimeRange(3000, 4000, 'relative:minute|30');
622
- });
623
- // Commit without refreshedTimeRange - should use draft values
624
- act(() => {
625
- result.current.commitDraft();
626
- });
627
- await waitFor(() => {
628
- expect(mockNavigateTo).toHaveBeenCalled();
629
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
630
- // Should use updated draft values
631
- expect(params.from).toBe('3000');
632
- expect(params.to).toBe('4000');
633
- expect(params.time_selection).toBe('relative:minute|30');
634
- });
635
- });
636
- });
637
- describe('State persistence after page reload', () => {
638
- it('should retain committed values after page reload simulation', async () => {
639
- // Initial state (using delta profile since sumBy only applies to delta)
640
- mockLocation.search =
641
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from=1000&to=2000';
642
- const { result: result1, unmount } = renderHook(() => useQueryState(), {
643
- wrapper: createWrapper(),
644
- });
645
- // User makes changes to draft (using delta profile since sumBy only applies to delta)
646
- act(() => {
647
- result1.current.setDraftExpression('memory:alloc_space:bytes:space:bytes:delta{}');
648
- result1.current.setDraftTimeRange(5000, 6000, 'relative:minute|15');
649
- result1.current.setDraftSumBy(['namespace', 'pod']);
650
- });
651
- // User clicks Search to commit
652
- act(() => {
653
- result1.current.commitDraft();
654
- });
655
- await waitFor(() => {
656
- expect(mockNavigateTo).toHaveBeenCalled();
657
- });
658
- // Get the params that were committed to URL
659
- const committedParams = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1][1];
660
- // Simulate page reload by updating mockLocation.search with committed values
661
- const queryString = new URLSearchParams({
662
- expression: committedParams.expression,
663
- from: committedParams.from,
664
- to: committedParams.to,
665
- time_selection: committedParams.time_selection,
666
- sum_by: committedParams.sum_by,
667
- }).toString();
668
- mockLocation.search = `?${queryString}`;
669
- // Unmount the old hook instance
670
- unmount();
671
- // Clear navigation mock to verify no new navigation on reload
672
- mockNavigateTo.mockClear();
673
- // Create new hook instance (simulating page reload)
674
- const { result: result2 } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
675
- // Verify state is loaded from URL after "reload"
676
- expect(result2.current.querySelection.expression).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
677
- expect(result2.current.querySelection.from).toBe(5000);
678
- expect(result2.current.querySelection.to).toBe(6000);
679
- expect(result2.current.querySelection.timeSelection).toBe('relative:minute|15');
680
- expect(result2.current.querySelection.sumBy).toEqual(['namespace', 'pod']);
681
- // Draft should be synced with URL state on page load
682
- expect(result2.current.draftSelection.expression).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
683
- expect(result2.current.draftSelection.from).toBe(5000);
684
- expect(result2.current.draftSelection.to).toBe(6000);
685
- expect(result2.current.draftSelection.sumBy).toEqual(['namespace', 'pod']);
686
- // No navigation should occur on page load
687
- expect(mockNavigateTo).not.toHaveBeenCalled();
688
- });
689
- it('should preserve delta profile merge params after reload', async () => {
690
- // Initial state with delta profile
691
- mockLocation.search =
692
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from=1000&to=2000';
693
- const { result: result1, unmount } = renderHook(() => useQueryState(), {
694
- wrapper: createWrapper(),
695
- });
696
- // Commit with time override
697
- act(() => {
698
- result1.current.commitDraft({
699
- from: 5000,
700
- to: 6000,
701
- timeSelection: 'relative:minute|5',
702
- });
703
- });
704
- await waitFor(() => {
705
- expect(mockNavigateTo).toHaveBeenCalled();
706
- });
707
- const committedParams = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1][1];
708
- // Verify merge params were set
709
- expect(committedParams.merge_from).toBe('5000000000');
710
- expect(committedParams.merge_to).toBe('6000000000');
711
- // Simulate page reload with all params including merge params
712
- const queryString = new URLSearchParams({
713
- expression: committedParams.expression,
714
- from: committedParams.from,
715
- to: committedParams.to,
716
- time_selection: committedParams.time_selection,
717
- merge_from: committedParams.merge_from,
718
- merge_to: committedParams.merge_to,
719
- }).toString();
720
- mockLocation.search = `?${queryString}`;
721
- unmount();
722
- mockNavigateTo.mockClear();
723
- // Create new hook instance
724
- const { result: result2 } = renderHook(() => useQueryState(), { wrapper: createWrapper() });
725
- // Verify merge params are preserved
726
- expect(result2.current.querySelection.mergeFrom).toBe('5000000000');
727
- expect(result2.current.querySelection.mergeTo).toBe('6000000000');
728
- // Draft should also have merge params
729
- expect(result2.current.draftSelection.mergeFrom).toBe('5000000000');
730
- expect(result2.current.draftSelection.mergeTo).toBe('6000000000');
731
- });
732
- });
733
- describe('ProfileSelection state management', () => {
734
- it('should initialize with null ProfileSelection when no URL params exist', () => {
735
- const { result } = renderHook(() => useQueryState({ suffix: '_a' }), { wrapper: createWrapper() });
736
- expect(result.current.profileSelection).toBeNull();
737
- });
738
- it('should compute ProfileSelection from URL params', () => {
739
- // Set URL with ProfileSelection params - using valid profile type
740
- mockLocation.search =
741
- '?merge_from_a=1234567890&merge_to_a=9876543210&selection_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{pod="test"}';
742
- const { result } = renderHook(() => useQueryState({ suffix: '_a' }), { wrapper: createWrapper() });
743
- const { profileSelection } = result.current;
744
- expect(profileSelection).not.toBeNull();
745
- // Test using the interface methods
746
- expect(profileSelection?.Type()).toBe('merge');
747
- expect(profileSelection?.ProfileName()).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta');
748
- // Test HistoryParams which should return merge params
749
- const historyParams = profileSelection?.HistoryParams();
750
- expect(historyParams?.merge_from).toBe('1234567890');
751
- expect(historyParams?.merge_to).toBe('9876543210');
752
- expect(historyParams?.selection).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{pod="test"}');
753
- });
754
- it('should auto-commit ProfileSelection to URL when setProfileSelection called', async () => {
755
- const { result } = renderHook(() => useQueryState({ suffix: '_a' }), { wrapper: createWrapper() });
756
- const mergeFrom = BigInt(5000000000);
757
- const mergeTo = BigInt(6000000000);
758
- // Create a mock Query object - in real code, this would be Query.parse()
759
- const mockQuery = {
760
- toString: () => 'memory:inuse_space:bytes:space:bytes{namespace="default"}',
761
- profileType: () => ({ delta: false }),
762
- };
763
- act(() => {
764
- result.current.setProfileSelection(mergeFrom, mergeTo, mockQuery);
765
- });
766
- await waitFor(() => {
767
- expect(mockNavigateTo).toHaveBeenCalled();
768
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
769
- expect(params.selection_a).toBe('memory:inuse_space:bytes:space:bytes{namespace="default"}');
770
- expect(params.merge_from_a).toBe('5000000000');
771
- expect(params.merge_to_a).toBe('6000000000');
772
- });
773
- });
774
- it('should use correct suffix for ProfileSelection in comparison mode', async () => {
775
- const { result: resultB } = renderHook(() => useQueryState({ suffix: '_b' }), {
776
- wrapper: createWrapper(),
777
- });
778
- const mergeFrom = BigInt(7000000000);
779
- const mergeTo = BigInt(8000000000);
780
- const mockQuery = {
781
- toString: () => 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{job="test"}',
782
- profileType: () => ({ delta: false }),
783
- };
784
- act(() => {
785
- resultB.current.setProfileSelection(mergeFrom, mergeTo, mockQuery);
786
- });
787
- await waitFor(() => {
788
- expect(mockNavigateTo).toHaveBeenCalled();
789
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
790
- expect(params.selection_b).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{job="test"}');
791
- expect(params.merge_from_b).toBe('7000000000');
792
- expect(params.merge_to_b).toBe('8000000000');
793
- });
794
- });
795
- it('should clear ProfileSelection when commitDraft is called', async () => {
796
- // Start with a ProfileSelection in URL - using valid profile type
797
- mockLocation.search =
798
- '?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&merge_from_a=1000000000&merge_to_a=2000000000&selection_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}';
799
- const { result } = renderHook(() => useQueryState({ suffix: '_a' }), { wrapper: createWrapper() });
800
- // Verify ProfileSelection exists
801
- expect(result.current.profileSelection).not.toBeNull();
802
- // Make a change to trigger commit
803
- act(() => {
804
- result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
805
- });
806
- // Commit the draft (this should clear ProfileSelection as per design decision 4.B)
807
- act(() => {
808
- result.current.commitDraft();
809
- });
810
- await waitFor(() => {
811
- expect(mockNavigateTo).toHaveBeenCalled();
812
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
813
- // ProfileSelection params should be cleared
814
- expect(params).not.toHaveProperty('selection_a');
815
- // But QuerySelection params should still be present
816
- expect(params.expression_a).toBe('memory:inuse_space:bytes:space:bytes{}');
817
- });
818
- });
819
- it('should handle ProfileSelection with delta profiles correctly', () => {
820
- mockLocation.search =
821
- '?merge_from_a=1000000000&merge_to_a=2000000000&selection_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{node="worker"}';
822
- const { result } = renderHook(() => useQueryState({ suffix: '_a' }), { wrapper: createWrapper() });
823
- const { profileSelection } = result.current;
824
- expect(profileSelection).not.toBeNull();
825
- // Test that ProfileSelection recognizes delta profile type
826
- expect(profileSelection?.ProfileName()).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta');
827
- // Test HistoryParams
828
- const historyParams = profileSelection?.HistoryParams();
829
- expect(historyParams?.merge_from).toBe('1000000000');
830
- expect(historyParams?.merge_to).toBe('2000000000');
831
- });
832
- it('should persist ProfileSelection across page reloads', async () => {
833
- // Initial state - user clicks on metrics graph point
834
- const { result: result1, unmount } = renderHook(() => useQueryState({ suffix: '_a' }), {
835
- wrapper: createWrapper(),
836
- });
837
- const mergeFrom = BigInt(3000000000);
838
- const mergeTo = BigInt(4000000000);
839
- const mockQuery = {
840
- toString: () => 'memory:alloc_objects:count:space:bytes{pod="test"}',
841
- profileType: () => ({ delta: false }),
842
- };
843
- // Set ProfileSelection
844
- act(() => {
845
- result1.current.setProfileSelection(mergeFrom, mergeTo, mockQuery);
846
- });
847
- await waitFor(() => {
848
- expect(mockNavigateTo).toHaveBeenCalled();
849
- });
850
- const committedParams = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1][1];
851
- // Simulate page reload by updating mockLocation.search
852
- const selectionA = String(committedParams.selection_a ?? '');
853
- const mergeFromA = String(committedParams.merge_from_a ?? '');
854
- const mergeToA = String(committedParams.merge_to_a ?? '');
855
- mockLocation.search = `?selection_a=${encodeURIComponent(selectionA)}&merge_from_a=${mergeFromA}&merge_to_a=${mergeToA}`;
856
- unmount();
857
- mockNavigateTo.mockClear();
858
- // Create new hook instance (simulating page reload)
859
- const { result: result2 } = renderHook(() => useQueryState({ suffix: '_a' }), {
860
- wrapper: createWrapper(),
861
- });
862
- // Verify ProfileSelection is loaded from URL after reload
863
- const profileSelection = result2.current.profileSelection;
864
- expect(profileSelection).not.toBeNull();
865
- // Use interface methods to test
866
- expect(profileSelection?.Type()).toBe('merge');
867
- const historyParams = profileSelection?.HistoryParams();
868
- expect(historyParams?.merge_from).toBe('3000000000');
869
- expect(historyParams?.merge_to).toBe('4000000000');
870
- expect(historyParams?.selection).toBe('memory:alloc_objects:count:space:bytes{pod="test"}');
871
- // No navigation should occur on page load
872
- expect(mockNavigateTo).not.toHaveBeenCalled();
873
- });
874
- it('should handle independent ProfileSelection for both sides in comparison mode', async () => {
875
- // Test component using both hooks with the same URLStateProvider (real-world scenario)
876
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
877
- const TestComponent = () => {
878
- const stateA = useQueryState({ suffix: '_a' });
879
- const stateB = useQueryState({ suffix: '_b' });
880
- return { stateA, stateB };
881
- };
882
- const { result } = renderHook(() => TestComponent(), {
883
- wrapper: createWrapper(),
884
- });
885
- const mockQueryA = {
886
- toString: () => 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="app-a"}',
887
- profileType: () => ({ delta: false }),
888
- };
889
- const mockQueryB = {
890
- toString: () => 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="app-b"}',
891
- profileType: () => ({ delta: false }),
892
- };
893
- // Set ProfileSelection for side A
894
- act(() => {
895
- result.current.stateA.setProfileSelection(BigInt(1000000000), BigInt(2000000000), mockQueryA);
896
- });
897
- await waitFor(() => {
898
- expect(mockNavigateTo).toHaveBeenCalled();
899
- });
900
- mockNavigateTo.mockClear();
901
- // Set ProfileSelection for side B
902
- act(() => {
903
- result.current.stateB.setProfileSelection(BigInt(3000000000), BigInt(4000000000), mockQueryB);
904
- });
905
- await waitFor(() => {
906
- expect(mockNavigateTo).toHaveBeenCalled();
907
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
908
- // Both selections should be in URL with different suffixes
909
- expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="app-a"}');
910
- expect(params.selection_b).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="app-b"}');
911
- expect(params.merge_from_a).toBe('1000000000');
912
- expect(params.merge_from_b).toBe('3000000000');
913
- });
914
- // The mockNavigateTo automatically updates mockLocation.search, so the URL change
915
- // should propagate to the hooks automatically. Verify both ProfileSelections exist.
916
- await waitFor(() => {
917
- expect(result.current.stateA.profileSelection).not.toBeNull();
918
- expect(result.current.stateB.profileSelection).not.toBeNull();
919
- });
920
- });
921
- it('should return null ProfileSelection when only partial params exist', () => {
922
- // Missing selection param
923
- mockLocation.search = '?merge_from_a=1000000000&merge_to_a=2000000000';
924
- const { result } = renderHook(() => useQueryState({ suffix: '_a' }), { wrapper: createWrapper() });
925
- expect(result.current.profileSelection).toBeNull();
926
- });
927
- it('should handle ProfileSelection with complex query expressions', async () => {
928
- const { result } = renderHook(() => useQueryState({ suffix: '_a' }), { wrapper: createWrapper() });
929
- const mockQuery = {
930
- toString: () => 'memory:alloc_objects:count:space:bytes:delta{namespace="default",pod="app-1",container="main"}',
931
- profileType: () => ({ delta: true }),
932
- };
933
- act(() => {
934
- result.current.setProfileSelection(BigInt(5000000000), BigInt(6000000000), mockQuery);
935
- });
936
- await waitFor(() => {
937
- expect(mockNavigateTo).toHaveBeenCalled();
938
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
939
- expect(params.selection_a).toBe('memory:alloc_objects:count:space:bytes:delta{namespace="default",pod="app-1",container="main"}');
940
- });
941
- });
942
- it('should batch ProfileSelection update with other URL state changes', async () => {
943
- const { result } = renderHook(() => useQueryState({ suffix: '_a' }), { wrapper: createWrapper() });
944
- const mockQuery = {
945
- toString: () => 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{job="test"}',
946
- profileType: () => ({ delta: false }),
947
- };
948
- // The batchUpdates is used internally by setProfileSelection
949
- act(() => {
950
- result.current.setProfileSelection(BigInt(1000000000), BigInt(2000000000), mockQuery);
951
- });
952
- await waitFor(() => {
953
- // Should only navigate once despite setting 3 params (selection, merge_from, merge_to)
954
- expect(mockNavigateTo).toHaveBeenCalledTimes(1);
955
- const [, params] = mockNavigateTo.mock.calls[0];
956
- expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{job="test"}');
957
- expect(params.merge_from_a).toBe('1000000000');
958
- expect(params.merge_to_a).toBe('2000000000');
959
- });
960
- });
961
- it('should preserve other URL params when setting ProfileSelection', async () => {
962
- mockLocation.search =
963
- '?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&other_param=value&unrelated=test';
964
- const { result } = renderHook(() => useQueryState({ suffix: '_a' }), { wrapper: createWrapper() });
965
- const mockQuery = {
966
- toString: () => 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}',
967
- profileType: () => ({ delta: false }),
968
- };
969
- act(() => {
970
- result.current.setProfileSelection(BigInt(1000000000), BigInt(2000000000), mockQuery);
971
- });
972
- await waitFor(() => {
973
- expect(mockNavigateTo).toHaveBeenCalled();
974
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
975
- // ProfileSelection params should be set
976
- expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}');
977
- // Other params should be preserved
978
- expect(params.expression_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{}');
979
- expect(params.other_param).toBe('value');
980
- expect(params.unrelated).toBe('test');
981
- });
982
- });
983
- });
984
- });