@parca/profile 0.19.140 → 0.19.143

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