@parca/profile 0.19.140 → 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 (253) hide show
  1. package/CHANGELOG.md +5 -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.d.ts.map +1 -1
  9. package/dist/GraphTooltipArrow/useGraphTooltipMetaInfo/index.js +104 -72
  10. package/dist/MatchersInput/SuggestionItem.js +91 -12
  11. package/dist/MatchersInput/SuggestionsList.d.ts +2 -1
  12. package/dist/MatchersInput/SuggestionsList.d.ts.map +1 -1
  13. package/dist/MatchersInput/SuggestionsList.js +371 -157
  14. package/dist/MatchersInput/SuggestionsList.test.d.ts +2 -0
  15. package/dist/MatchersInput/SuggestionsList.test.d.ts.map +1 -0
  16. package/dist/MatchersInput/index.js +308 -115
  17. package/dist/MetricsCircle/index.js +39 -3
  18. package/dist/MetricsGraph/MetricsContextMenu/index.js +119 -19
  19. package/dist/MetricsGraph/MetricsInfoPanel/index.js +81 -20
  20. package/dist/MetricsGraph/MetricsTooltip/index.d.ts.map +1 -1
  21. package/dist/MetricsGraph/MetricsTooltip/index.js +107 -74
  22. package/dist/MetricsGraph/index.js +552 -203
  23. package/dist/MetricsGraph/useMetricsGraphDimensions.js +46 -25
  24. package/dist/MetricsGraph/utils/colorMapping.js +24 -17
  25. package/dist/MetricsSeries/index.js +70 -7
  26. package/dist/PreSelectedMatchers/index.d.ts.map +1 -1
  27. package/dist/PreSelectedMatchers/index.js +249 -102
  28. package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts.map +1 -1
  29. package/dist/ProfileExplorer/ProfileExplorerCompare.js +241 -45
  30. package/dist/ProfileExplorer/ProfileExplorerSingle.js +98 -11
  31. package/dist/ProfileExplorer/index.js +183 -32
  32. package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.js +333 -148
  33. package/dist/ProfileFlameChart/SamplesStrips/SamplesStrips.stories.js +69 -35
  34. package/dist/ProfileFlameChart/SamplesStrips/index.d.ts +2 -2
  35. package/dist/ProfileFlameChart/SamplesStrips/index.d.ts.map +1 -1
  36. package/dist/ProfileFlameChart/SamplesStrips/index.js +645 -134
  37. package/dist/ProfileFlameChart/SamplesStrips/labelSetUtils.js +114 -55
  38. package/dist/ProfileFlameChart/index.d.ts.map +1 -1
  39. package/dist/ProfileFlameChart/index.js +267 -129
  40. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.d.ts.map +1 -1
  41. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.js +288 -89
  42. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenuWrapper.js +56 -20
  43. package/dist/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.js +211 -140
  44. package/dist/ProfileFlameGraph/FlameGraphArrow/MemoizedTooltip.js +133 -38
  45. package/dist/ProfileFlameGraph/FlameGraphArrow/MiniMap.js +261 -216
  46. package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.d.ts.map +1 -1
  47. package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.js +72 -47
  48. package/dist/ProfileFlameGraph/FlameGraphArrow/TooltipContext.d.ts.map +1 -1
  49. package/dist/ProfileFlameGraph/FlameGraphArrow/TooltipContext.js +58 -28
  50. package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.d.ts.map +1 -1
  51. package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.js +59 -8
  52. package/dist/ProfileFlameGraph/FlameGraphArrow/index.js +396 -179
  53. package/dist/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.d.ts.map +1 -1
  54. package/dist/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.js +68 -50
  55. package/dist/ProfileFlameGraph/FlameGraphArrow/useMappingList.js +62 -38
  56. package/dist/ProfileFlameGraph/FlameGraphArrow/useNodeColor.js +14 -6
  57. package/dist/ProfileFlameGraph/FlameGraphArrow/useScrollViewport.js +124 -82
  58. package/dist/ProfileFlameGraph/FlameGraphArrow/useVisibleNodes.js +160 -98
  59. package/dist/ProfileFlameGraph/FlameGraphArrow/useZoom.js +232 -112
  60. package/dist/ProfileFlameGraph/FlameGraphArrow/utils.js +137 -114
  61. package/dist/ProfileFlameGraph/benchmarks/benchdata/populateData.js +85 -0
  62. package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
  63. package/dist/ProfileFlameGraph/index.js +324 -150
  64. package/dist/ProfileMetricsGraph/hooks/useQueryRange.js +140 -32
  65. package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
  66. package/dist/ProfileMetricsGraph/index.js +519 -258
  67. package/dist/ProfileSelector/CompareButton.js +132 -12
  68. package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
  69. package/dist/ProfileSelector/MetricsGraphSection.js +236 -64
  70. package/dist/ProfileSelector/index.d.ts.map +1 -1
  71. package/dist/ProfileSelector/index.js +727 -141
  72. package/dist/ProfileSelector/useAutoQuerySelector.js +249 -130
  73. package/dist/ProfileSource.js +230 -163
  74. package/dist/ProfileTypeSelector/index.js +214 -125
  75. package/dist/ProfileView/components/ActionButtons/GroupByDropdown.js +50 -4
  76. package/dist/ProfileView/components/ActionButtons/SortByDropdown.d.ts.map +1 -1
  77. package/dist/ProfileView/components/ActionButtons/SortByDropdown.js +141 -35
  78. package/dist/ProfileView/components/ColorStackLegend.d.ts.map +1 -1
  79. package/dist/ProfileView/components/ColorStackLegend.js +185 -55
  80. package/dist/ProfileView/components/DashboardItems/index.js +87 -28
  81. package/dist/ProfileView/components/DashboardLayout/index.js +108 -16
  82. package/dist/ProfileView/components/DiffLegend.js +172 -29
  83. package/dist/ProfileView/components/GroupByLabelsDropdown/index.js +199 -55
  84. package/dist/ProfileView/components/InvertCallStack/index.d.ts.map +1 -1
  85. package/dist/ProfileView/components/InvertCallStack/index.js +100 -12
  86. package/dist/ProfileView/components/ProfileFilters/filterPresets.js +260 -315
  87. package/dist/ProfileView/components/ProfileFilters/index.js +518 -215
  88. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.js +370 -306
  89. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts +2 -1
  90. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
  91. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +188 -118
  92. package/dist/ProfileView/components/ProfileHeader/index.js +105 -11
  93. package/dist/ProfileView/components/ShareButton/ResultBox.js +119 -16
  94. package/dist/ProfileView/components/ShareButton/index.js +352 -62
  95. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts.map +1 -1
  96. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +678 -194
  97. package/dist/ProfileView/components/Toolbars/SwitchMenuItem.js +94 -7
  98. package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.d.ts.map +1 -1
  99. package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.js +199 -157
  100. package/dist/ProfileView/components/Toolbars/index.d.ts +2 -2
  101. package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
  102. package/dist/ProfileView/components/Toolbars/index.js +441 -21
  103. package/dist/ProfileView/components/ViewSelector/Dropdown.js +233 -22
  104. package/dist/ProfileView/components/ViewSelector/index.d.ts.map +1 -1
  105. package/dist/ProfileView/components/ViewSelector/index.js +212 -86
  106. package/dist/ProfileView/components/VisualizationContainer/index.d.ts.map +1 -1
  107. package/dist/ProfileView/components/VisualizationContainer/index.js +52 -7
  108. package/dist/ProfileView/components/VisualizationPanel.js +185 -8
  109. package/dist/ProfileView/context/DashboardContext.d.ts.map +1 -1
  110. package/dist/ProfileView/context/DashboardContext.js +85 -29
  111. package/dist/ProfileView/context/ProfileViewContext.js +56 -15
  112. package/dist/ProfileView/hooks/useAutoSelectDimension.js +71 -41
  113. package/dist/ProfileView/hooks/useProfileMetadata.js +50 -18
  114. package/dist/ProfileView/hooks/useResetFlameGraphState.d.ts.map +1 -1
  115. package/dist/ProfileView/hooks/useResetFlameGraphState.js +32 -12
  116. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.d.ts.map +1 -1
  117. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.js +71 -27
  118. package/dist/ProfileView/hooks/useResetStateOnSeriesChange.d.ts.map +1 -1
  119. package/dist/ProfileView/hooks/useResetStateOnSeriesChange.js +40 -19
  120. package/dist/ProfileView/hooks/useVisualizationState.d.ts +3 -3
  121. package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
  122. package/dist/ProfileView/hooks/useVisualizationState.js +258 -67
  123. package/dist/ProfileView/index.js +383 -45
  124. package/dist/ProfileView/types/visualization.js +1 -13
  125. package/dist/ProfileView/utils/colorUtils.js +8 -7
  126. package/dist/ProfileViewWithData.d.ts.map +1 -1
  127. package/dist/ProfileViewWithData.js +332 -228
  128. package/dist/QueryControls/index.js +418 -47
  129. package/dist/Sandwich/components/CalleesSection.js +54 -4
  130. package/dist/Sandwich/components/CallersSection.js +97 -27
  131. package/dist/Sandwich/components/TableSection.js +77 -4
  132. package/dist/Sandwich/index.d.ts.map +1 -1
  133. package/dist/Sandwich/index.js +126 -14
  134. package/dist/Sandwich/utils/processRowData.js +48 -39
  135. package/dist/SelectWithRefresh/index.js +102 -28
  136. package/dist/SimpleMatchers/Select.js +520 -187
  137. package/dist/SimpleMatchers/index.js +590 -288
  138. package/dist/SourceView/Highlighter.js +230 -70
  139. package/dist/SourceView/LineNo.js +72 -17
  140. package/dist/SourceView/index.d.ts.map +1 -1
  141. package/dist/SourceView/index.js +178 -104
  142. package/dist/SourceView/lang-detector/ext-to-lang.json +798 -798
  143. package/dist/SourceView/lang-detector/index.js +28 -14
  144. package/dist/SourceView/useSelectedLineRange.d.ts.map +1 -1
  145. package/dist/SourceView/useSelectedLineRange.js +99 -23
  146. package/dist/Table/ColorCell.js +42 -1
  147. package/dist/Table/ColumnsVisibility.js +114 -6
  148. package/dist/Table/MoreDropdown.d.ts.map +1 -1
  149. package/dist/Table/MoreDropdown.js +122 -25
  150. package/dist/Table/TableContextMenu.d.ts.map +1 -1
  151. package/dist/Table/TableContextMenu.js +151 -137
  152. package/dist/Table/TableContextMenuWrapper.js +59 -14
  153. package/dist/Table/hooks/useColorManagement.js +58 -16
  154. package/dist/Table/hooks/useTableConfiguration.d.ts.map +1 -1
  155. package/dist/Table/hooks/useTableConfiguration.js +333 -169
  156. package/dist/Table/index.d.ts.map +1 -1
  157. package/dist/Table/index.js +222 -128
  158. package/dist/Table/utils/functions.js +169 -144
  159. package/dist/Table/utils/topAndBottomExpandedRowModel.js +69 -52
  160. package/dist/TimelineGuide/index.js +209 -16
  161. package/dist/TopTable/benchmarks/benchdata/populateData.js +91 -0
  162. package/dist/TopTable/index.d.ts.map +1 -1
  163. package/dist/TopTable/index.js +342 -123
  164. package/dist/contexts/LabelsQueryProvider.js +94 -32
  165. package/dist/contexts/UnifiedLabelsContext.js +114 -49
  166. package/dist/contexts/utils.js +37 -15
  167. package/dist/hooks/useCompareModeMeta.d.ts.map +1 -1
  168. package/dist/hooks/useCompareModeMeta.js +158 -64
  169. package/dist/hooks/useLabels.js +295 -52
  170. package/dist/hooks/useQueryState.d.ts +3 -3
  171. package/dist/hooks/useQueryState.d.ts.map +1 -1
  172. package/dist/hooks/useQueryState.js +373 -332
  173. package/dist/index.d.ts +2 -3
  174. package/dist/index.d.ts.map +1 -1
  175. package/dist/index.js +22 -8
  176. package/dist/testdata/fg-diff.json +3750 -0
  177. package/dist/testdata/fg-simple.json +1879 -0
  178. package/dist/testdata/link_data.json +56 -0
  179. package/dist/testdata/tabular.json +30 -0
  180. package/dist/testdata/test_flamegraph.json +26846 -0
  181. package/dist/testdata/test_graph.json +53 -0
  182. package/dist/useDelayedLoader.js +32 -18
  183. package/dist/useGrpcQuery/index.js +71 -11
  184. package/dist/useHasProfileData.js +90 -12
  185. package/dist/useQuery.js +205 -64
  186. package/dist/useSumBy.d.ts +1 -1
  187. package/dist/useSumBy.d.ts.map +1 -1
  188. package/dist/useSumBy.js +294 -138
  189. package/dist/utils.js +62 -30
  190. package/package.json +9 -10
  191. package/src/GraphTooltipArrow/index.tsx +3 -0
  192. package/src/GraphTooltipArrow/useGraphTooltipMetaInfo/index.ts +13 -11
  193. package/src/MatchersInput/SuggestionsList.test.tsx +70 -0
  194. package/src/MatchersInput/SuggestionsList.tsx +11 -10
  195. package/src/MatchersInput/index.tsx +1 -1
  196. package/src/MetricsGraph/MetricsTooltip/index.tsx +22 -34
  197. package/src/PreSelectedMatchers/index.tsx +3 -0
  198. package/src/ProfileExplorer/ProfileExplorerCompare.tsx +9 -4
  199. package/src/ProfileFlameChart/SamplesStrips/index.tsx +2 -2
  200. package/src/ProfileFlameChart/index.tsx +28 -21
  201. package/src/ProfileFlameGraph/FlameGraphArrow/ContextMenu.tsx +9 -10
  202. package/src/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.tsx +6 -5
  203. package/src/ProfileFlameGraph/FlameGraphArrow/TooltipContext.tsx +3 -0
  204. package/src/ProfileFlameGraph/FlameGraphArrow/ZoomControls.tsx +3 -0
  205. package/src/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.ts +3 -0
  206. package/src/ProfileFlameGraph/index.tsx +9 -6
  207. package/src/ProfileMetricsGraph/index.tsx +8 -6
  208. package/src/ProfileSelector/MetricsGraphSection.tsx +10 -5
  209. package/src/ProfileSelector/index.tsx +61 -39
  210. package/src/ProfileView/components/ActionButtons/SortByDropdown.tsx +6 -10
  211. package/src/ProfileView/components/ColorStackLegend.tsx +4 -2
  212. package/src/ProfileView/components/InvertCallStack/index.tsx +4 -5
  213. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.tsx +192 -94
  214. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +21 -21
  215. package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +28 -24
  216. package/src/ProfileView/components/Toolbars/TableColumnsDropdown.tsx +5 -4
  217. package/src/ProfileView/components/Toolbars/index.tsx +3 -3
  218. package/src/ProfileView/components/ViewSelector/index.tsx +16 -9
  219. package/src/ProfileView/components/VisualizationContainer/index.tsx +3 -0
  220. package/src/ProfileView/context/DashboardContext.tsx +6 -6
  221. package/src/ProfileView/hooks/useResetFlameGraphState.ts +4 -6
  222. package/src/ProfileView/hooks/useResetStateOnProfileTypeChange.ts +26 -24
  223. package/src/ProfileView/hooks/useResetStateOnSeriesChange.ts +8 -16
  224. package/src/ProfileView/hooks/useVisualizationState.ts +69 -61
  225. package/src/ProfileViewWithData.tsx +35 -29
  226. package/src/Sandwich/index.tsx +3 -4
  227. package/src/SourceView/index.tsx +2 -4
  228. package/src/SourceView/useSelectedLineRange.ts +19 -34
  229. package/src/Table/MoreDropdown.tsx +11 -9
  230. package/src/Table/TableContextMenu.tsx +13 -10
  231. package/src/Table/hooks/useTableConfiguration.tsx +11 -16
  232. package/src/Table/index.tsx +21 -12
  233. package/src/TopTable/index.tsx +4 -3
  234. package/src/hooks/useCompareModeMeta.ts +91 -61
  235. package/src/hooks/useQueryState.test.tsx +345 -275
  236. package/src/hooks/useQueryState.ts +118 -136
  237. package/src/index.tsx +15 -16
  238. package/src/useDelayedLoader.ts +10 -10
  239. package/src/useSumBy.ts +15 -21
  240. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.js +0 -455
  241. package/dist/hooks/urlParsers.d.ts +0 -18
  242. package/dist/hooks/urlParsers.d.ts.map +0 -1
  243. package/dist/hooks/urlParsers.js +0 -32
  244. package/dist/hooks/useColorBy.d.ts +0 -5
  245. package/dist/hooks/useColorBy.d.ts.map +0 -1
  246. package/dist/hooks/useColorBy.js +0 -26
  247. package/dist/hooks/useDashboardItems.d.ts +0 -5
  248. package/dist/hooks/useDashboardItems.d.ts.map +0 -1
  249. package/dist/hooks/useDashboardItems.js +0 -27
  250. package/dist/hooks/useQueryState.test.js +0 -868
  251. package/src/hooks/urlParsers.ts +0 -38
  252. package/src/hooks/useColorBy.ts +0 -42
  253. package/src/hooks/useDashboardItems.ts +0 -46
@@ -15,28 +15,78 @@ import {type ReactNode} from 'react';
15
15
 
16
16
  // eslint-disable-next-line import/named
17
17
  import {act, renderHook, waitFor} from '@testing-library/react';
18
- // eslint-disable-next-line import/no-unresolved
19
- import {NuqsTestingAdapter, type OnUrlUpdateFunction} from 'nuqs/adapters/testing';
20
- import {describe, expect, it, vi} from 'vitest';
18
+ import {beforeEach, describe, expect, it, vi} from 'vitest';
19
+
20
+ import {URLStateProvider} from '@parca/components';
21
21
 
22
22
  import {type ProfileFilter} from './useProfileFilters';
23
23
  import {decodeProfileFilters, useProfileFiltersUrlState} from './useProfileFiltersUrlState';
24
24
 
25
- // Helper to create wrapper with NuqsTestingAdapter
26
- const createWrapper = (
27
- searchParams: string | Record<string, string> = {},
28
- onUrlUpdate?: OnUrlUpdateFunction
29
- ): (({children}: {children: ReactNode}) => JSX.Element) => {
25
+ // Mock window.location
26
+ const mockLocation = {
27
+ pathname: '/test',
28
+ search: '',
29
+ };
30
+
31
+ // Mock the navigate function
32
+ const mockNavigateTo = vi.fn((path: string, params: Record<string, string | string[]>) => {
33
+ const searchParams = new URLSearchParams();
34
+ Object.entries(params).forEach(([key, value]) => {
35
+ if (value !== undefined && value !== null) {
36
+ if (Array.isArray(value)) {
37
+ searchParams.set(key, value.join(','));
38
+ } else {
39
+ searchParams.set(key, String(value));
40
+ }
41
+ }
42
+ });
43
+ mockLocation.search = `?${searchParams.toString()}`;
44
+ });
45
+
46
+ // Mock getQueryParamsFromURL
47
+ vi.mock('@parca/components/src/hooks/URLState/utils', async () => {
48
+ const actual = await vi.importActual('@parca/components/src/hooks/URLState/utils');
49
+ return {
50
+ ...actual,
51
+ getQueryParamsFromURL: () => {
52
+ if (mockLocation.search === '') return {};
53
+ const params = new URLSearchParams(mockLocation.search);
54
+ const result: Record<string, string | string[]> = {};
55
+ for (const [key, value] of params.entries()) {
56
+ const decodedValue = decodeURIComponent(value);
57
+ const existing = result[key];
58
+ if (existing !== undefined) {
59
+ result[key] = Array.isArray(existing)
60
+ ? [...existing, decodedValue]
61
+ : [existing, decodedValue];
62
+ } else {
63
+ result[key] = decodedValue;
64
+ }
65
+ }
66
+ return result;
67
+ },
68
+ };
69
+ });
70
+
71
+ // Helper to create wrapper with URLStateProvider
72
+ const createWrapper = (): (({children}: {children: ReactNode}) => JSX.Element) => {
30
73
  const Wrapper = ({children}: {children: ReactNode}): JSX.Element => (
31
- <NuqsTestingAdapter searchParams={searchParams} onUrlUpdate={onUrlUpdate} hasMemory={true}>
32
- {children}
33
- </NuqsTestingAdapter>
74
+ <URLStateProvider navigateTo={mockNavigateTo}>{children}</URLStateProvider>
34
75
  );
35
- Wrapper.displayName = 'NuqsTestingWrapper';
76
+ Wrapper.displayName = 'URLStateProviderWrapper';
36
77
  return Wrapper;
37
78
  };
38
79
 
39
80
  describe('useProfileFiltersUrlState', () => {
81
+ beforeEach(() => {
82
+ mockNavigateTo.mockClear();
83
+ Object.defineProperty(window, 'location', {
84
+ value: mockLocation,
85
+ writable: true,
86
+ });
87
+ mockLocation.search = '';
88
+ });
89
+
40
90
  describe('decodeProfileFilters', () => {
41
91
  it('should return empty array for empty string', () => {
42
92
  expect(decodeProfileFilters('')).toEqual([]);
@@ -205,9 +255,9 @@ describe('useProfileFiltersUrlState', () => {
205
255
  });
206
256
 
207
257
  it('should read filters from URL', async () => {
208
- const {result} = renderHook(() => useProfileFiltersUrlState(), {
209
- wrapper: createWrapper({profile_filters: 's:fn:=:testFunc'}),
210
- });
258
+ mockLocation.search = '?profile_filters=s:fn:=:testFunc';
259
+
260
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
211
261
 
212
262
  await waitFor(() => {
213
263
  expect(result.current.appliedFilters).toHaveLength(1);
@@ -221,10 +271,7 @@ describe('useProfileFiltersUrlState', () => {
221
271
  });
222
272
 
223
273
  it('should update URL when setting filters', async () => {
224
- const onUrlUpdate = vi.fn();
225
- const {result} = renderHook(() => useProfileFiltersUrlState(), {
226
- wrapper: createWrapper({}, onUrlUpdate),
227
- });
274
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
228
275
 
229
276
  const newFilters: ProfileFilter[] = [
230
277
  {
@@ -241,26 +288,26 @@ describe('useProfileFiltersUrlState', () => {
241
288
  });
242
289
 
243
290
  await waitFor(() => {
244
- expect(onUrlUpdate).toHaveBeenCalled();
245
- const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
246
- expect(lastCall.searchParams.get('profile_filters')).toBe('f:b:!~:libc.so');
291
+ expect(mockNavigateTo).toHaveBeenCalled();
292
+ const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
293
+ expect(params.profile_filters).toBe('f:b:!~:libc.so');
247
294
  });
248
295
  });
249
296
 
250
297
  it('should clear URL param when setting empty filters', async () => {
251
- const onUrlUpdate = vi.fn();
252
- const {result} = renderHook(() => useProfileFiltersUrlState(), {
253
- wrapper: createWrapper({profile_filters: 's:fn:=:testFunc'}, onUrlUpdate),
254
- });
298
+ mockLocation.search = '?profile_filters=s:fn:=:testFunc';
299
+
300
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
255
301
 
256
302
  act(() => {
257
303
  result.current.setAppliedFilters([]);
258
304
  });
259
305
 
260
306
  await waitFor(() => {
261
- expect(onUrlUpdate).toHaveBeenCalled();
262
- const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
263
- expect(lastCall.searchParams.has('profile_filters')).toBe(false);
307
+ expect(mockNavigateTo).toHaveBeenCalled();
308
+ const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
309
+ // When filters are empty, the param is either empty string or undefined (removed)
310
+ expect(params.profile_filters === '' || params.profile_filters === undefined).toBe(true);
264
311
  });
265
312
  });
266
313
  });
@@ -273,10 +320,9 @@ describe('useProfileFiltersUrlState', () => {
273
320
  });
274
321
 
275
322
  it('should force apply filters overwriting existing', async () => {
276
- const onUrlUpdate = vi.fn();
277
- const {result} = renderHook(() => useProfileFiltersUrlState(), {
278
- wrapper: createWrapper({profile_filters: 's:fn:=:existingFunc'}, onUrlUpdate),
279
- });
323
+ mockLocation.search = '?profile_filters=s:fn:=:existingFunc';
324
+
325
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
280
326
 
281
327
  // Verify existing filter is loaded
282
328
  await waitFor(() => {
@@ -298,36 +344,33 @@ describe('useProfileFiltersUrlState', () => {
298
344
  });
299
345
 
300
346
  await waitFor(() => {
301
- expect(onUrlUpdate).toHaveBeenCalled();
302
- const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
303
- expect(lastCall.searchParams.get('profile_filters')).toBe('f:b:!~:forcedValue');
347
+ expect(mockNavigateTo).toHaveBeenCalled();
348
+ const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
349
+ expect(params.profile_filters).toBe('f:b:!~:forcedValue');
304
350
  });
305
351
  });
306
352
 
307
353
  it('should clear filters when force applying empty array', async () => {
308
- const onUrlUpdate = vi.fn();
309
- const {result} = renderHook(() => useProfileFiltersUrlState(), {
310
- wrapper: createWrapper({profile_filters: 's:fn:=:existingFunc'}, onUrlUpdate),
311
- });
354
+ mockLocation.search = '?profile_filters=s:fn:=:existingFunc';
355
+
356
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
312
357
 
313
358
  act(() => {
314
359
  result.current.forceApplyFilters([]);
315
360
  });
316
361
 
317
362
  await waitFor(() => {
318
- expect(onUrlUpdate).toHaveBeenCalled();
319
- const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
320
- expect(lastCall.searchParams.has('profile_filters')).toBe(false);
363
+ expect(mockNavigateTo).toHaveBeenCalled();
364
+ const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
365
+ // When filters are empty, the param is either empty string or undefined (removed)
366
+ expect(params.profile_filters === '' || params.profile_filters === undefined).toBe(true);
321
367
  });
322
368
  });
323
369
  });
324
370
 
325
371
  describe('Preset filter encoding', () => {
326
372
  it('should encode preset filters correctly', async () => {
327
- const onUrlUpdate = vi.fn();
328
- const {result} = renderHook(() => useProfileFiltersUrlState(), {
329
- wrapper: createWrapper({}, onUrlUpdate),
330
- });
373
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
331
374
 
332
375
  const presetFilters: ProfileFilter[] = [
333
376
  {
@@ -342,17 +385,14 @@ describe('useProfileFiltersUrlState', () => {
342
385
  });
343
386
 
344
387
  await waitFor(() => {
345
- expect(onUrlUpdate).toHaveBeenCalled();
346
- const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
347
- expect(lastCall.searchParams.get('profile_filters')).toBe('p:hide_libc:enabled');
388
+ expect(mockNavigateTo).toHaveBeenCalled();
389
+ const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
390
+ expect(params.profile_filters).toBe('p:hide_libc:enabled');
348
391
  });
349
392
  });
350
393
 
351
394
  it('should handle mixed preset and regular filters', async () => {
352
- const onUrlUpdate = vi.fn();
353
- const {result} = renderHook(() => useProfileFiltersUrlState(), {
354
- wrapper: createWrapper({}, onUrlUpdate),
355
- });
395
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
356
396
 
357
397
  const mixedFilters: ProfileFilter[] = [
358
398
  {
@@ -374,21 +414,16 @@ describe('useProfileFiltersUrlState', () => {
374
414
  });
375
415
 
376
416
  await waitFor(() => {
377
- expect(onUrlUpdate).toHaveBeenCalled();
378
- const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
379
- expect(lastCall.searchParams.get('profile_filters')).toBe(
380
- 'p:hide_libc:enabled,f:b:!~:node'
381
- );
417
+ expect(mockNavigateTo).toHaveBeenCalled();
418
+ const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
419
+ expect(params.profile_filters).toBe('p:hide_libc:enabled,f:b:!~:node');
382
420
  });
383
421
  });
384
422
  });
385
423
 
386
424
  describe('URL encoding edge cases', () => {
387
425
  it('should handle special characters in filter values', async () => {
388
- const onUrlUpdate = vi.fn();
389
- const {result} = renderHook(() => useProfileFiltersUrlState(), {
390
- wrapper: createWrapper({}, onUrlUpdate),
391
- });
426
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
392
427
 
393
428
  const filtersWithSpecialChars: ProfileFilter[] = [
394
429
  {
@@ -405,19 +440,15 @@ describe('useProfileFiltersUrlState', () => {
405
440
  });
406
441
 
407
442
  await waitFor(() => {
408
- expect(onUrlUpdate).toHaveBeenCalled();
409
- const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
410
- const filterValue = lastCall.searchParams.get('profile_filters');
411
- // The value should contain the encoded special characters
412
- expect(filterValue).toContain('std%3A%3Avector%3Cint%3E');
443
+ expect(mockNavigateTo).toHaveBeenCalled();
444
+ const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
445
+ // Value should be URL encoded
446
+ expect(params.profile_filters).toContain('std%3A%3Avector%3Cint%3E');
413
447
  });
414
448
  });
415
449
 
416
450
  it('should filter out incomplete filters when encoding', async () => {
417
- const onUrlUpdate = vi.fn();
418
- const {result} = renderHook(() => useProfileFiltersUrlState(), {
419
- wrapper: createWrapper({}, onUrlUpdate),
420
- });
451
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
421
452
 
422
453
  const incompleteFilters: ProfileFilter[] = [
423
454
  {
@@ -445,10 +476,10 @@ describe('useProfileFiltersUrlState', () => {
445
476
  });
446
477
 
447
478
  await waitFor(() => {
448
- expect(onUrlUpdate).toHaveBeenCalled();
449
- const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
479
+ expect(mockNavigateTo).toHaveBeenCalled();
480
+ const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
450
481
  // Only the complete filter should be encoded
451
- expect(lastCall.searchParams.get('profile_filters')).toBe('f:b:!~:valid');
482
+ expect(params.profile_filters).toBe('f:b:!~:valid');
452
483
  });
453
484
  });
454
485
  });
@@ -470,9 +501,9 @@ describe('useProfileFiltersUrlState', () => {
470
501
  });
471
502
 
472
503
  it('should return correctly structured filters from URL', async () => {
473
- const {result} = renderHook(() => useProfileFiltersUrlState(), {
474
- wrapper: createWrapper({profile_filters: 's:fn:=:testFunc'}),
475
- });
504
+ mockLocation.search = '?profile_filters=s:fn:=:testFunc';
505
+
506
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
476
507
 
477
508
  await waitFor(() => {
478
509
  expect(result.current.appliedFilters).toHaveLength(1);
@@ -491,16 +522,15 @@ describe('useProfileFiltersUrlState', () => {
491
522
 
492
523
  describe('View switching scenarios', () => {
493
524
  it('should completely replace filters when switching views using forceApplyFilters', async () => {
494
- const onUrlUpdate = vi.fn();
495
- const {result} = renderHook(() => useProfileFiltersUrlState(), {
496
- wrapper: createWrapper(
497
- {profile_filters: 's:fn:=:viewAFunc,f:b:!=:viewABinary'},
498
- onUrlUpdate
499
- ),
500
- });
525
+ // Start with View A's filters (2 filters)
526
+ mockLocation.search = '?profile_filters=s:fn:=:viewAFunc,f:b:!=:viewABinary';
527
+
528
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
501
529
 
502
530
  await waitFor(() => {
503
531
  expect(result.current.appliedFilters).toHaveLength(2);
532
+ expect(result.current.appliedFilters[0].value).toBe('viewAFunc');
533
+ expect(result.current.appliedFilters[1].value).toBe('viewABinary');
504
534
  });
505
535
 
506
536
  // Switch to View B (completely different filter)
@@ -519,28 +549,96 @@ describe('useProfileFiltersUrlState', () => {
519
549
  });
520
550
 
521
551
  await waitFor(() => {
522
- expect(onUrlUpdate).toHaveBeenCalled();
523
- const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
524
- const filterValue = lastCall.searchParams.get('profile_filters');
552
+ expect(mockNavigateTo).toHaveBeenCalled();
553
+ const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
525
554
 
526
555
  // View A's filters should be completely gone
527
- expect(filterValue).not.toContain('viewAFunc');
528
- expect(filterValue).not.toContain('viewABinary');
556
+ expect(params.profile_filters).not.toContain('viewAFunc');
557
+ expect(params.profile_filters).not.toContain('viewABinary');
529
558
 
530
559
  // Only View B's filter should be present
531
- expect(filterValue).toBe('f:fn:~:viewBOnly');
560
+ expect(params.profile_filters).toBe('f:fn:~:viewBOnly');
532
561
  });
533
562
  });
534
563
 
535
- it('should not change filters when clicking the same view tab', async () => {
536
- const {result} = renderHook(() => useProfileFiltersUrlState(), {
537
- wrapper: createWrapper({profile_filters: 's:fn:=:existingFilter'}),
564
+ it('should handle sequential view switches correctly', async () => {
565
+ // Simulate: [default] -> [storage] -> [testing-view]
566
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
567
+
568
+ // View 1: default view (1 filter)
569
+ const defaultFilters: ProfileFilter[] = [{id: 'd-1', type: 'hide_libc', value: 'enabled'}];
570
+
571
+ act(() => {
572
+ result.current.forceApplyFilters(defaultFilters);
538
573
  });
539
574
 
575
+ await waitFor(() => {
576
+ expect(mockNavigateTo).toHaveBeenCalled();
577
+ const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
578
+ expect(params.profile_filters).toBe('p:hide_libc:enabled');
579
+ });
580
+
581
+ mockNavigateTo.mockClear();
582
+
583
+ // View 2: storage view (3 filters)
584
+ const storageFilters: ProfileFilter[] = [
585
+ {id: 's-1', type: 'stack', field: 'function_name', matchType: 'not_contains', value: 'io'},
586
+ {id: 's-2', type: 'frame', field: 'binary', matchType: 'not_contains', value: 'disk'},
587
+ {id: 's-3', type: 'frame', field: 'function_name', matchType: 'contains', value: 'storage'},
588
+ ];
589
+
590
+ act(() => {
591
+ result.current.forceApplyFilters(storageFilters);
592
+ });
593
+
594
+ await waitFor(() => {
595
+ expect(mockNavigateTo).toHaveBeenCalled();
596
+ const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
597
+ // Default view's filter should be gone
598
+ expect(params.profile_filters).not.toContain('hide_libc');
599
+ // Storage view should have 3 filters
600
+ expect(params.profile_filters).toContain('io');
601
+ expect(params.profile_filters).toContain('disk');
602
+ expect(params.profile_filters).toContain('storage');
603
+ });
604
+
605
+ mockNavigateTo.mockClear();
606
+
607
+ // View 3: testing-view (2 filters)
608
+ const testingFilters: ProfileFilter[] = [
609
+ {id: 't-1', type: 'stack', field: 'function_name', matchType: 'equal', value: 'test_main'},
610
+ {id: 't-2', type: 'frame', field: 'binary', matchType: 'contains', value: 'test'},
611
+ ];
612
+
613
+ act(() => {
614
+ result.current.forceApplyFilters(testingFilters);
615
+ });
616
+
617
+ await waitFor(() => {
618
+ expect(mockNavigateTo).toHaveBeenCalled();
619
+ const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
620
+ // Storage view's filters should be gone
621
+ expect(params.profile_filters).not.toContain('io');
622
+ expect(params.profile_filters).not.toContain('disk');
623
+ expect(params.profile_filters).not.toContain('storage');
624
+ // Testing view should have its 2 filters
625
+ expect(params.profile_filters).toContain('test_main');
626
+ expect(params.profile_filters).toContain('test');
627
+ });
628
+ });
629
+
630
+ it('should not change filters when clicking the same view tab', async () => {
631
+ // Start with existing filters
632
+ mockLocation.search = '?profile_filters=s:fn:=:existingFilter';
633
+
634
+ const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
635
+
540
636
  await waitFor(() => {
541
637
  expect(result.current.appliedFilters).toHaveLength(1);
542
638
  });
543
639
 
640
+ mockNavigateTo.mockClear();
641
+
544
642
  // Apply the same filters (simulating clicking the same view tab)
545
643
  const sameFilters: ProfileFilter[] = [
546
644
  {
@@ -13,8 +13,7 @@
13
13
 
14
14
  import {useCallback, useMemo} from 'react';
15
15
 
16
- import {createParser, useQueryState} from 'nuqs';
17
-
16
+ import {useURLStateBatch, useURLStateCustom, type ParamValueSetterCustom} from '@parca/components';
18
17
  import {safeDecode} from '@parca/utilities';
19
18
 
20
19
  import {isPresetKey} from './filterPresets';
@@ -138,32 +137,31 @@ export const decodeProfileFilters = (encoded: string): ProfileFilter[] => {
138
137
  }
139
138
  };
140
139
 
141
- const profileFiltersParser = createParser<ProfileFilter[]>({
142
- parse: (value: string) => decodeProfileFilters(value),
143
- serialize: (value: ProfileFilter[]) => encodeProfileFilters(value),
144
- eq: (a, b) => encodeProfileFilters(a) === encodeProfileFilters(b),
145
- })
146
- .withDefault([])
147
- .withOptions({history: 'replace'});
148
-
149
140
  export const useProfileFiltersUrlState = (): {
150
141
  appliedFilters: ProfileFilter[];
151
- setAppliedFilters: (filters: ProfileFilter[]) => void;
142
+ setAppliedFilters: ParamValueSetterCustom<ProfileFilter[]>;
152
143
  forceApplyFilters: (filters: ProfileFilter[]) => void;
153
144
  } => {
154
- const [appliedFilters, setRawFilters] = useQueryState('profile_filters', profileFiltersParser);
145
+ const batchUpdates = useURLStateBatch();
146
+
147
+ // Store applied filters in URL state for persistence using compact encoding
148
+ const [appliedFilters, setAppliedFilters] = useURLStateCustom<ProfileFilter[]>(
149
+ `profile_filters`,
150
+ {
151
+ parse: value => {
152
+ return decodeProfileFilters(value as string);
153
+ },
154
+ stringify: value => {
155
+ return encodeProfileFilters(value);
156
+ },
157
+ defaultValue: [],
158
+ }
159
+ );
155
160
 
156
161
  const memoizedAppliedFilters = useMemo(() => {
157
162
  return appliedFilters ?? [];
158
163
  }, [appliedFilters]);
159
164
 
160
- const setAppliedFilters = useCallback(
161
- (filters: ProfileFilter[]) => {
162
- void setRawFilters(filters);
163
- },
164
- [setRawFilters]
165
- );
166
-
167
165
  // Force apply filters (bypasses preserve-existing strategy)
168
166
  const forceApplyFilters = useCallback(
169
167
  (filters: ProfileFilter[]) => {
@@ -174,9 +172,11 @@ export const useProfileFiltersUrlState = (): {
174
172
  return f.value !== '' && f.type != null && f.field != null && f.matchType != null;
175
173
  });
176
174
 
177
- setAppliedFilters(validFilters);
175
+ batchUpdates(() => {
176
+ setAppliedFilters(validFilters);
177
+ });
178
178
  },
179
- [setAppliedFilters]
179
+ [batchUpdates, setAppliedFilters]
180
180
  );
181
181
 
182
182
  return {
@@ -11,13 +11,15 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
+ /* eslint-disable react-hooks/set-state-in-effect */
15
+
14
16
  import React, {useCallback, useEffect, useRef, useState} from 'react';
15
17
 
16
18
  import {Menu} from '@headlessui/react';
17
19
  import {Icon} from '@iconify/react';
18
20
  import cx from 'classnames';
19
- import {useQueryState} from 'nuqs';
20
21
 
22
+ import {useURLState} from '@parca/components';
21
23
  import {USER_PREFERENCES, useUserPreference} from '@parca/hooks';
22
24
  import {ProfileType} from '@parca/parser';
23
25
 
@@ -27,7 +29,6 @@ import {
27
29
  FIELD_LOCATION_ADDRESS,
28
30
  FIELD_MAPPING_FILE,
29
31
  } from '../../../ProfileFlameGraph/FlameGraphArrow';
30
- import {boolParam, hiddenBinariesParser, stringParam} from '../../../hooks/urlParsers';
31
32
  import {useProfileViewContext} from '../../context/ProfileViewContext';
32
33
  import SwitchMenuItem from './SwitchMenuItem';
33
34
 
@@ -74,6 +75,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
74
75
  customSubmenu,
75
76
  renderAsDiv = false,
76
77
  }) => {
78
+ 'use no memo';
77
79
  const menuRef = useRef<HTMLDivElement>(null);
78
80
  const [shouldOpenLeft, setShouldOpenLeft] = useState(false);
79
81
 
@@ -207,15 +209,14 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
207
209
  }) => {
208
210
  const dropdownRef = useRef<HTMLDivElement>(null);
209
211
  const [shouldOpenLeft, setShouldOpenLeft] = useState(false);
210
- const [storeSortBy] = useQueryState('sort_by', stringParam.withDefault(FIELD_FUNCTION_NAME));
211
- const [colorStackLegend, setStoreColorStackLegend] = useQueryState(
212
- 'color_stack_legend',
213
- stringParam
214
- );
215
- const [hiddenBinaries, setHiddenBinaries] = useQueryState(
216
- 'hidden_binaries',
217
- hiddenBinariesParser
218
- );
212
+ const [storeSortBy] = useURLState('sort_by', {
213
+ defaultValue: FIELD_FUNCTION_NAME,
214
+ });
215
+ const [colorStackLegend, setStoreColorStackLegend] = useURLState('color_stack_legend');
216
+ const [hiddenBinaries, setHiddenBinaries] = useURLState('hidden_binaries', {
217
+ defaultValue: [],
218
+ alwaysReturnArray: true,
219
+ });
219
220
  const {compareMode} = useProfileViewContext();
220
221
  const [colorProfileName] = useUserPreference<string>(
221
222
  USER_PREFERENCES.FLAMEGRAPH_COLOR_PROFILE.key
@@ -225,10 +226,11 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
225
226
 
226
227
  // By default, we want delta profiles (CPU) to be relatively compared.
227
228
  // For non-delta profiles, like goroutines or memory, we want the profiles to be compared absolutely.
228
- const compareAbsoluteDefault = profileType?.delta === false;
229
+ const compareAbsoluteDefault = profileType?.delta === false ? 'true' : 'false';
229
230
 
230
- const [compareAbsolute, setCompareAbsolute] = useQueryState('compare_absolute', boolParam);
231
- const isCompareAbsolute = compareAbsolute ?? compareAbsoluteDefault;
231
+ const [compareAbsolute = compareAbsoluteDefault, setCompareAbsolute] =
232
+ useURLState('compare_absolute');
233
+ const isCompareAbsolute = compareAbsolute === 'true';
232
234
 
233
235
  useEffect(() => {
234
236
  const checkOverflow = (): void => {
@@ -249,20 +251,20 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
249
251
  }, [isTableVizOnly]);
250
252
 
251
253
  const handleBinaryToggle = (index: number): void => {
252
- const updatedBinaries = [...hiddenBinaries];
254
+ const updatedBinaries = [...(hiddenBinaries as string[])];
253
255
  updatedBinaries.splice(index, 1);
254
- void setHiddenBinaries(updatedBinaries);
256
+ setHiddenBinaries(updatedBinaries);
255
257
  };
256
258
 
257
259
  const setColorStackLegend = useCallback(
258
260
  (value: string): void => {
259
- void setStoreColorStackLegend(value);
261
+ setStoreColorStackLegend(value);
260
262
  },
261
263
  [setStoreColorStackLegend]
262
264
  );
263
265
 
264
266
  const resetLegend = (): void => {
265
- void setHiddenBinaries([]);
267
+ setHiddenBinaries([]);
266
268
  };
267
269
 
268
270
  const menuItems: MenuItemType[] = [
@@ -330,7 +332,7 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
330
332
  },
331
333
  {
332
334
  label: isCompareAbsolute ? 'Compare Relative' : 'Compare Absolute',
333
- onclick: () => void setCompareAbsolute(!isCompareAbsolute),
335
+ onclick: () => setCompareAbsolute(isCompareAbsolute ? 'false' : 'true'),
334
336
  hide: !compareMode,
335
337
  icon: isCompareAbsolute ? 'fluent-mdl2:compare' : 'fluent-mdl2:compare-uneven',
336
338
  },
@@ -360,7 +362,7 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
360
362
  },
361
363
  {
362
364
  label: 'Reset Legend',
363
- hide: hiddenBinaries.length === 0,
365
+ hide: hiddenBinaries === undefined || hiddenBinaries.length === 0,
364
366
  onclick: () => resetLegend(),
365
367
  id: 'h-reset-legend-button',
366
368
  icon: 'system-uicons:reset',
@@ -368,7 +370,7 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
368
370
  {
369
371
  label: 'Hidden Binaries',
370
372
  id: 'h-hidden-binaries',
371
- items: hiddenBinaries.map((binary, index) => ({
373
+ items: (hiddenBinaries as string[])?.map((binary, index) => ({
372
374
  label: binary,
373
375
  customSubmenu: (
374
376
  <div className="flex items-center gap-2 w-full">
@@ -384,7 +386,7 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
384
386
  </div>
385
387
  ),
386
388
  })),
387
- hide: hiddenBinaries.length === 0,
389
+ hide: hiddenBinaries === undefined || hiddenBinaries.length === 0,
388
390
  icon: 'ph:eye-closed',
389
391
  },
390
392
  ];
@@ -425,8 +427,10 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
425
427
  {...item}
426
428
  onSelect={onSelect}
427
429
  closeDropdown={close}
428
- activeValueForSortBy={storeSortBy}
429
- activeValueForColorBy={colorBy}
430
+ activeValueForSortBy={storeSortBy as string}
431
+ activeValueForColorBy={
432
+ colorBy === undefined || colorBy === '' ? 'binary' : colorBy
433
+ }
430
434
  activeValuesForLevel={groupBy}
431
435
  renderAsDiv={item.renderAsDiv}
432
436
  />