@parca/profile 0.19.139 → 0.19.140

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 (132) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/GraphTooltipArrow/useGraphTooltipMetaInfo/index.d.ts.map +1 -1
  3. package/dist/GraphTooltipArrow/useGraphTooltipMetaInfo/index.js +11 -13
  4. package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts.map +1 -1
  5. package/dist/ProfileExplorer/ProfileExplorerCompare.js +4 -9
  6. package/dist/ProfileFlameChart/SamplesStrips/index.d.ts +2 -2
  7. package/dist/ProfileFlameChart/SamplesStrips/index.d.ts.map +1 -1
  8. package/dist/ProfileFlameChart/index.d.ts.map +1 -1
  9. package/dist/ProfileFlameChart/index.js +13 -19
  10. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.d.ts.map +1 -1
  11. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.js +8 -8
  12. package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.d.ts.map +1 -1
  13. package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.js +4 -3
  14. package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
  15. package/dist/ProfileFlameGraph/index.js +6 -4
  16. package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
  17. package/dist/ProfileMetricsGraph/index.js +4 -6
  18. package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
  19. package/dist/ProfileSelector/MetricsGraphSection.js +5 -10
  20. package/dist/ProfileSelector/index.d.ts.map +1 -1
  21. package/dist/ProfileSelector/index.js +27 -25
  22. package/dist/ProfileView/components/ActionButtons/SortByDropdown.d.ts.map +1 -1
  23. package/dist/ProfileView/components/ActionButtons/SortByDropdown.js +5 -5
  24. package/dist/ProfileView/components/ColorStackLegend.d.ts.map +1 -1
  25. package/dist/ProfileView/components/ColorStackLegend.js +2 -3
  26. package/dist/ProfileView/components/InvertCallStack/index.d.ts.map +1 -1
  27. package/dist/ProfileView/components/InvertCallStack/index.js +5 -4
  28. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts +1 -2
  29. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
  30. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +14 -16
  31. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.js +84 -170
  32. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts.map +1 -1
  33. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +16 -20
  34. package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.d.ts.map +1 -1
  35. package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.js +4 -5
  36. package/dist/ProfileView/components/Toolbars/index.d.ts +2 -2
  37. package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
  38. package/dist/ProfileView/components/Toolbars/index.js +1 -1
  39. package/dist/ProfileView/components/ViewSelector/index.d.ts.map +1 -1
  40. package/dist/ProfileView/components/ViewSelector/index.js +8 -14
  41. package/dist/ProfileView/context/DashboardContext.d.ts.map +1 -1
  42. package/dist/ProfileView/context/DashboardContext.js +6 -6
  43. package/dist/ProfileView/hooks/useResetFlameGraphState.d.ts.map +1 -1
  44. package/dist/ProfileView/hooks/useResetFlameGraphState.js +5 -4
  45. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.d.ts.map +1 -1
  46. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.js +25 -26
  47. package/dist/ProfileView/hooks/useResetStateOnSeriesChange.d.ts.map +1 -1
  48. package/dist/ProfileView/hooks/useResetStateOnSeriesChange.js +13 -8
  49. package/dist/ProfileView/hooks/useVisualizationState.d.ts +3 -3
  50. package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
  51. package/dist/ProfileView/hooks/useVisualizationState.js +35 -51
  52. package/dist/ProfileViewWithData.d.ts.map +1 -1
  53. package/dist/ProfileViewWithData.js +19 -28
  54. package/dist/Sandwich/index.d.ts.map +1 -1
  55. package/dist/Sandwich/index.js +4 -3
  56. package/dist/SourceView/index.d.ts.map +1 -1
  57. package/dist/SourceView/index.js +4 -2
  58. package/dist/SourceView/useSelectedLineRange.d.ts.map +1 -1
  59. package/dist/SourceView/useSelectedLineRange.js +21 -16
  60. package/dist/Table/MoreDropdown.d.ts.map +1 -1
  61. package/dist/Table/MoreDropdown.js +8 -11
  62. package/dist/Table/TableContextMenu.d.ts.map +1 -1
  63. package/dist/Table/TableContextMenu.js +10 -13
  64. package/dist/Table/hooks/useTableConfiguration.d.ts.map +1 -1
  65. package/dist/Table/hooks/useTableConfiguration.js +3 -4
  66. package/dist/Table/index.d.ts.map +1 -1
  67. package/dist/Table/index.js +11 -9
  68. package/dist/TopTable/index.d.ts.map +1 -1
  69. package/dist/TopTable/index.js +3 -4
  70. package/dist/hooks/urlParsers.d.ts +18 -0
  71. package/dist/hooks/urlParsers.d.ts.map +1 -0
  72. package/dist/hooks/urlParsers.js +32 -0
  73. package/dist/hooks/useColorBy.d.ts +5 -0
  74. package/dist/hooks/useColorBy.d.ts.map +1 -0
  75. package/dist/hooks/useColorBy.js +26 -0
  76. package/dist/hooks/useCompareModeMeta.d.ts.map +1 -1
  77. package/dist/hooks/useCompareModeMeta.js +55 -86
  78. package/dist/hooks/useDashboardItems.d.ts +5 -0
  79. package/dist/hooks/useDashboardItems.d.ts.map +1 -0
  80. package/dist/hooks/useDashboardItems.js +27 -0
  81. package/dist/hooks/useQueryState.d.ts +3 -3
  82. package/dist/hooks/useQueryState.d.ts.map +1 -1
  83. package/dist/hooks/useQueryState.js +105 -105
  84. package/dist/hooks/useQueryState.test.js +186 -302
  85. package/dist/index.d.ts +3 -2
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +3 -12
  88. package/dist/useSumBy.d.ts +1 -1
  89. package/dist/useSumBy.d.ts.map +1 -1
  90. package/dist/useSumBy.js +2 -2
  91. package/package.json +8 -7
  92. package/src/GraphTooltipArrow/useGraphTooltipMetaInfo/index.ts +11 -13
  93. package/src/ProfileExplorer/ProfileExplorerCompare.tsx +4 -9
  94. package/src/ProfileFlameChart/SamplesStrips/index.tsx +2 -2
  95. package/src/ProfileFlameChart/index.tsx +21 -28
  96. package/src/ProfileFlameGraph/FlameGraphArrow/ContextMenu.tsx +10 -9
  97. package/src/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.tsx +5 -3
  98. package/src/ProfileFlameGraph/index.tsx +6 -9
  99. package/src/ProfileMetricsGraph/index.tsx +6 -8
  100. package/src/ProfileSelector/MetricsGraphSection.tsx +5 -10
  101. package/src/ProfileSelector/index.tsx +32 -31
  102. package/src/ProfileView/components/ActionButtons/SortByDropdown.tsx +10 -6
  103. package/src/ProfileView/components/ColorStackLegend.tsx +2 -4
  104. package/src/ProfileView/components/InvertCallStack/index.tsx +5 -4
  105. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.tsx +94 -192
  106. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +21 -21
  107. package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +24 -25
  108. package/src/ProfileView/components/Toolbars/TableColumnsDropdown.tsx +4 -5
  109. package/src/ProfileView/components/Toolbars/index.tsx +3 -3
  110. package/src/ProfileView/components/ViewSelector/index.tsx +9 -16
  111. package/src/ProfileView/context/DashboardContext.tsx +6 -6
  112. package/src/ProfileView/hooks/useResetFlameGraphState.ts +6 -4
  113. package/src/ProfileView/hooks/useResetStateOnProfileTypeChange.ts +24 -26
  114. package/src/ProfileView/hooks/useResetStateOnSeriesChange.ts +16 -8
  115. package/src/ProfileView/hooks/useVisualizationState.ts +61 -69
  116. package/src/ProfileViewWithData.tsx +29 -35
  117. package/src/Sandwich/index.tsx +4 -3
  118. package/src/SourceView/index.tsx +4 -2
  119. package/src/SourceView/useSelectedLineRange.ts +34 -19
  120. package/src/Table/MoreDropdown.tsx +9 -11
  121. package/src/Table/TableContextMenu.tsx +10 -13
  122. package/src/Table/hooks/useTableConfiguration.tsx +3 -4
  123. package/src/Table/index.tsx +12 -21
  124. package/src/TopTable/index.tsx +3 -4
  125. package/src/hooks/urlParsers.ts +38 -0
  126. package/src/hooks/useColorBy.ts +42 -0
  127. package/src/hooks/useCompareModeMeta.ts +61 -91
  128. package/src/hooks/useDashboardItems.ts +46 -0
  129. package/src/hooks/useQueryState.test.tsx +275 -345
  130. package/src/hooks/useQueryState.ts +136 -118
  131. package/src/index.tsx +16 -15
  132. package/src/useSumBy.ts +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/InvertCallStack/index.tsx"],"names":[],"mappings":"AAoBA,QAAA,MAAM,eAAe,QAAO,GAAG,CAAC,OA0B/B,CAAC;AAEF,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/InvertCallStack/index.tsx"],"names":[],"mappings":"AAsBA,QAAA,MAAM,eAAe,QAAO,GAAG,CAAC,OAyB/B,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -12,15 +12,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
14
  import { Icon } from '@iconify/react';
15
- import { Button, useURLState } from '@parca/components';
15
+ import { useQueryState } from 'nuqs';
16
+ import { Button } from '@parca/components';
16
17
  import { TEST_IDS, testId } from '@parca/test-utils';
18
+ import { invertCallStackParser } from '../../../hooks/urlParsers';
17
19
  import { useResetFlameGraphState } from '../../hooks/useResetFlameGraphState';
18
20
  const InvertCallStack = () => {
19
- const [invertStack = '', setInvertStack] = useURLState('invert_call_stack');
20
- const isInvert = invertStack === 'true';
21
+ const [isInvert, setInvertStack] = useQueryState('invert_call_stack', invertCallStackParser);
21
22
  const resetFlameGraphState = useResetFlameGraphState();
22
23
  const handleSetInvert = (value) => {
23
- setInvertStack(value ? 'true' : '');
24
+ void setInvertStack(value);
24
25
  resetFlameGraphState();
25
26
  };
26
27
  return (_jsxs("div", { className: "flex flex-col", children: [_jsx("label", { className: "text-sm", children: "\u00A0" }), _jsxs(Button, { variant: "neutral", className: "flex items-center gap-2 whitespace-nowrap", onClick: () => handleSetInvert(!isInvert), id: "h-invert-call-stack", ...testId(TEST_IDS.INVERT_CALL_STACK_BUTTON), children: [_jsx(Icon, { icon: isInvert ? 'ph:sort-ascending' : 'ph:sort-descending', className: "h-4 w-4" }), isInvert ? 'Original' : 'Invert', " Call Stack"] })] }));
@@ -1,9 +1,8 @@
1
- import { type ParamValueSetterCustom } from '@parca/components';
2
1
  import { type ProfileFilter } from './useProfileFilters';
3
2
  export declare const decodeProfileFilters: (encoded: string) => ProfileFilter[];
4
3
  export declare const useProfileFiltersUrlState: () => {
5
4
  appliedFilters: ProfileFilter[];
6
- setAppliedFilters: ParamValueSetterCustom<ProfileFilter[]>;
5
+ setAppliedFilters: (filters: ProfileFilter[]) => void;
7
6
  forceApplyFilters: (filters: ProfileFilter[]) => void;
8
7
  };
9
8
  //# sourceMappingURL=useProfileFiltersUrlState.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useProfileFiltersUrlState.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts"],"names":[],"mappings":"AAeA,OAAO,EAAsC,KAAK,sBAAsB,EAAC,MAAM,mBAAmB,CAAC;AAInG,OAAO,EAAC,KAAK,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAsEvD,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,aAAa,EAgDnE,CAAC;AAEF,eAAO,MAAM,yBAAyB,QAAO;IAC3C,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,iBAAiB,EAAE,sBAAsB,CAAC,aAAa,EAAE,CAAC,CAAC;IAC3D,iBAAiB,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;CA4CvD,CAAC"}
1
+ {"version":3,"file":"useProfileFiltersUrlState.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAC,KAAK,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAsEvD,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,aAAa,EAgDnE,CAAC;AAUF,eAAO,MAAM,yBAAyB,QAAO;IAC3C,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,iBAAiB,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;IACtD,iBAAiB,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;CAmCvD,CAAC"}
@@ -11,7 +11,7 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
  import { useCallback, useMemo } from 'react';
14
- import { useURLStateBatch, useURLStateCustom } from '@parca/components';
14
+ import { createParser, useQueryState } from 'nuqs';
15
15
  import { safeDecode } from '@parca/utilities';
16
16
  import { isPresetKey } from './filterPresets';
17
17
  // Compact encoding mappings
@@ -113,21 +113,21 @@ export const decodeProfileFilters = (encoded) => {
113
113
  return [];
114
114
  }
115
115
  };
116
+ const profileFiltersParser = createParser({
117
+ parse: (value) => decodeProfileFilters(value),
118
+ serialize: (value) => encodeProfileFilters(value),
119
+ eq: (a, b) => encodeProfileFilters(a) === encodeProfileFilters(b),
120
+ })
121
+ .withDefault([])
122
+ .withOptions({ history: 'replace' });
116
123
  export const useProfileFiltersUrlState = () => {
117
- const batchUpdates = useURLStateBatch();
118
- // Store applied filters in URL state for persistence using compact encoding
119
- const [appliedFilters, setAppliedFilters] = useURLStateCustom(`profile_filters`, {
120
- parse: value => {
121
- return decodeProfileFilters(value);
122
- },
123
- stringify: value => {
124
- return encodeProfileFilters(value);
125
- },
126
- defaultValue: [],
127
- });
124
+ const [appliedFilters, setRawFilters] = useQueryState('profile_filters', profileFiltersParser);
128
125
  const memoizedAppliedFilters = useMemo(() => {
129
126
  return appliedFilters ?? [];
130
127
  }, [appliedFilters]);
128
+ const setAppliedFilters = useCallback((filters) => {
129
+ void setRawFilters(filters);
130
+ }, [setRawFilters]);
131
131
  // Force apply filters (bypasses preserve-existing strategy)
132
132
  const forceApplyFilters = useCallback((filters) => {
133
133
  const validFilters = filters.filter(f => {
@@ -136,10 +136,8 @@ export const useProfileFiltersUrlState = () => {
136
136
  }
137
137
  return f.value !== '' && f.type != null && f.field != null && f.matchType != null;
138
138
  });
139
- batchUpdates(() => {
140
- setAppliedFilters(validFilters);
141
- });
142
- }, [batchUpdates, setAppliedFilters]);
139
+ setAppliedFilters(validFilters);
140
+ }, [setAppliedFilters]);
143
141
  return {
144
142
  appliedFilters: memoizedAppliedFilters,
145
143
  setAppliedFilters,
@@ -1,70 +1,17 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  // eslint-disable-next-line import/named
3
3
  import { act, renderHook, waitFor } from '@testing-library/react';
4
- import { beforeEach, describe, expect, it, vi } from 'vitest';
5
- import { URLStateProvider } from '@parca/components';
4
+ // eslint-disable-next-line import/no-unresolved
5
+ import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
6
+ import { describe, expect, it, vi } from 'vitest';
6
7
  import { decodeProfileFilters, useProfileFiltersUrlState } from './useProfileFiltersUrlState';
7
- // Mock window.location
8
- const mockLocation = {
9
- pathname: '/test',
10
- search: '',
11
- };
12
- // Mock the navigate function
13
- const mockNavigateTo = vi.fn((path, params) => {
14
- const searchParams = new URLSearchParams();
15
- Object.entries(params).forEach(([key, value]) => {
16
- if (value !== undefined && value !== null) {
17
- if (Array.isArray(value)) {
18
- searchParams.set(key, value.join(','));
19
- }
20
- else {
21
- searchParams.set(key, String(value));
22
- }
23
- }
24
- });
25
- mockLocation.search = `?${searchParams.toString()}`;
26
- });
27
- // Mock getQueryParamsFromURL
28
- vi.mock('@parca/components/src/hooks/URLState/utils', async () => {
29
- const actual = await vi.importActual('@parca/components/src/hooks/URLState/utils');
30
- return {
31
- ...actual,
32
- getQueryParamsFromURL: () => {
33
- if (mockLocation.search === '')
34
- return {};
35
- const params = new URLSearchParams(mockLocation.search);
36
- const result = {};
37
- for (const [key, value] of params.entries()) {
38
- const decodedValue = decodeURIComponent(value);
39
- const existing = result[key];
40
- if (existing !== undefined) {
41
- result[key] = Array.isArray(existing)
42
- ? [...existing, decodedValue]
43
- : [existing, decodedValue];
44
- }
45
- else {
46
- result[key] = decodedValue;
47
- }
48
- }
49
- return result;
50
- },
51
- };
52
- });
53
- // Helper to create wrapper with URLStateProvider
54
- const createWrapper = () => {
55
- const Wrapper = ({ children }) => (_jsx(URLStateProvider, { navigateTo: mockNavigateTo, children: children }));
56
- Wrapper.displayName = 'URLStateProviderWrapper';
8
+ // Helper to create wrapper with NuqsTestingAdapter
9
+ const createWrapper = (searchParams = {}, onUrlUpdate) => {
10
+ const Wrapper = ({ children }) => (_jsx(NuqsTestingAdapter, { searchParams: searchParams, onUrlUpdate: onUrlUpdate, hasMemory: true, children: children }));
11
+ Wrapper.displayName = 'NuqsTestingWrapper';
57
12
  return Wrapper;
58
13
  };
59
14
  describe('useProfileFiltersUrlState', () => {
60
- beforeEach(() => {
61
- mockNavigateTo.mockClear();
62
- Object.defineProperty(window, 'location', {
63
- value: mockLocation,
64
- writable: true,
65
- });
66
- mockLocation.search = '';
67
- });
68
15
  describe('decodeProfileFilters', () => {
69
16
  it('should return empty array for empty string', () => {
70
17
  expect(decodeProfileFilters('')).toEqual([]);
@@ -206,8 +153,9 @@ describe('useProfileFiltersUrlState', () => {
206
153
  expect(result.current.appliedFilters).toEqual([]);
207
154
  });
208
155
  it('should read filters from URL', async () => {
209
- mockLocation.search = '?profile_filters=s:fn:=:testFunc';
210
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
156
+ const { result } = renderHook(() => useProfileFiltersUrlState(), {
157
+ wrapper: createWrapper({ profile_filters: 's:fn:=:testFunc' }),
158
+ });
211
159
  await waitFor(() => {
212
160
  expect(result.current.appliedFilters).toHaveLength(1);
213
161
  expect(result.current.appliedFilters[0]).toMatchObject({
@@ -219,7 +167,10 @@ describe('useProfileFiltersUrlState', () => {
219
167
  });
220
168
  });
221
169
  it('should update URL when setting filters', async () => {
222
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
170
+ const onUrlUpdate = vi.fn();
171
+ const { result } = renderHook(() => useProfileFiltersUrlState(), {
172
+ wrapper: createWrapper({}, onUrlUpdate),
173
+ });
223
174
  const newFilters = [
224
175
  {
225
176
  id: 'test-1',
@@ -233,22 +184,23 @@ describe('useProfileFiltersUrlState', () => {
233
184
  result.current.setAppliedFilters(newFilters);
234
185
  });
235
186
  await waitFor(() => {
236
- expect(mockNavigateTo).toHaveBeenCalled();
237
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
238
- expect(params.profile_filters).toBe('f:b:!~:libc.so');
187
+ expect(onUrlUpdate).toHaveBeenCalled();
188
+ const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
189
+ expect(lastCall.searchParams.get('profile_filters')).toBe('f:b:!~:libc.so');
239
190
  });
240
191
  });
241
192
  it('should clear URL param when setting empty filters', async () => {
242
- mockLocation.search = '?profile_filters=s:fn:=:testFunc';
243
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
193
+ const onUrlUpdate = vi.fn();
194
+ const { result } = renderHook(() => useProfileFiltersUrlState(), {
195
+ wrapper: createWrapper({ profile_filters: 's:fn:=:testFunc' }, onUrlUpdate),
196
+ });
244
197
  act(() => {
245
198
  result.current.setAppliedFilters([]);
246
199
  });
247
200
  await waitFor(() => {
248
- expect(mockNavigateTo).toHaveBeenCalled();
249
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
250
- // When filters are empty, the param is either empty string or undefined (removed)
251
- expect(params.profile_filters === '' || params.profile_filters === undefined).toBe(true);
201
+ expect(onUrlUpdate).toHaveBeenCalled();
202
+ const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
203
+ expect(lastCall.searchParams.has('profile_filters')).toBe(false);
252
204
  });
253
205
  });
254
206
  });
@@ -258,8 +210,10 @@ describe('useProfileFiltersUrlState', () => {
258
210
  expect(typeof result.current.forceApplyFilters).toBe('function');
259
211
  });
260
212
  it('should force apply filters overwriting existing', async () => {
261
- mockLocation.search = '?profile_filters=s:fn:=:existingFunc';
262
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
213
+ const onUrlUpdate = vi.fn();
214
+ const { result } = renderHook(() => useProfileFiltersUrlState(), {
215
+ wrapper: createWrapper({ profile_filters: 's:fn:=:existingFunc' }, onUrlUpdate),
216
+ });
263
217
  // Verify existing filter is loaded
264
218
  await waitFor(() => {
265
219
  expect(result.current.appliedFilters).toHaveLength(1);
@@ -277,28 +231,32 @@ describe('useProfileFiltersUrlState', () => {
277
231
  result.current.forceApplyFilters(newFilters);
278
232
  });
279
233
  await waitFor(() => {
280
- expect(mockNavigateTo).toHaveBeenCalled();
281
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
282
- expect(params.profile_filters).toBe('f:b:!~:forcedValue');
234
+ expect(onUrlUpdate).toHaveBeenCalled();
235
+ const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
236
+ expect(lastCall.searchParams.get('profile_filters')).toBe('f:b:!~:forcedValue');
283
237
  });
284
238
  });
285
239
  it('should clear filters when force applying empty array', async () => {
286
- mockLocation.search = '?profile_filters=s:fn:=:existingFunc';
287
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
240
+ const onUrlUpdate = vi.fn();
241
+ const { result } = renderHook(() => useProfileFiltersUrlState(), {
242
+ wrapper: createWrapper({ profile_filters: 's:fn:=:existingFunc' }, onUrlUpdate),
243
+ });
288
244
  act(() => {
289
245
  result.current.forceApplyFilters([]);
290
246
  });
291
247
  await waitFor(() => {
292
- expect(mockNavigateTo).toHaveBeenCalled();
293
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
294
- // When filters are empty, the param is either empty string or undefined (removed)
295
- expect(params.profile_filters === '' || params.profile_filters === undefined).toBe(true);
248
+ expect(onUrlUpdate).toHaveBeenCalled();
249
+ const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
250
+ expect(lastCall.searchParams.has('profile_filters')).toBe(false);
296
251
  });
297
252
  });
298
253
  });
299
254
  describe('Preset filter encoding', () => {
300
255
  it('should encode preset filters correctly', async () => {
301
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
256
+ const onUrlUpdate = vi.fn();
257
+ const { result } = renderHook(() => useProfileFiltersUrlState(), {
258
+ wrapper: createWrapper({}, onUrlUpdate),
259
+ });
302
260
  const presetFilters = [
303
261
  {
304
262
  id: 'preset-1',
@@ -310,13 +268,16 @@ describe('useProfileFiltersUrlState', () => {
310
268
  result.current.setAppliedFilters(presetFilters);
311
269
  });
312
270
  await waitFor(() => {
313
- expect(mockNavigateTo).toHaveBeenCalled();
314
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
315
- expect(params.profile_filters).toBe('p:hide_libc:enabled');
271
+ expect(onUrlUpdate).toHaveBeenCalled();
272
+ const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
273
+ expect(lastCall.searchParams.get('profile_filters')).toBe('p:hide_libc:enabled');
316
274
  });
317
275
  });
318
276
  it('should handle mixed preset and regular filters', async () => {
319
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
277
+ const onUrlUpdate = vi.fn();
278
+ const { result } = renderHook(() => useProfileFiltersUrlState(), {
279
+ wrapper: createWrapper({}, onUrlUpdate),
280
+ });
320
281
  const mixedFilters = [
321
282
  {
322
283
  id: 'preset-1',
@@ -335,15 +296,18 @@ describe('useProfileFiltersUrlState', () => {
335
296
  result.current.setAppliedFilters(mixedFilters);
336
297
  });
337
298
  await waitFor(() => {
338
- expect(mockNavigateTo).toHaveBeenCalled();
339
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
340
- expect(params.profile_filters).toBe('p:hide_libc:enabled,f:b:!~:node');
299
+ expect(onUrlUpdate).toHaveBeenCalled();
300
+ const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
301
+ expect(lastCall.searchParams.get('profile_filters')).toBe('p:hide_libc:enabled,f:b:!~:node');
341
302
  });
342
303
  });
343
304
  });
344
305
  describe('URL encoding edge cases', () => {
345
306
  it('should handle special characters in filter values', async () => {
346
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
307
+ const onUrlUpdate = vi.fn();
308
+ const { result } = renderHook(() => useProfileFiltersUrlState(), {
309
+ wrapper: createWrapper({}, onUrlUpdate),
310
+ });
347
311
  const filtersWithSpecialChars = [
348
312
  {
349
313
  id: 'special-1',
@@ -357,14 +321,18 @@ describe('useProfileFiltersUrlState', () => {
357
321
  result.current.setAppliedFilters(filtersWithSpecialChars);
358
322
  });
359
323
  await waitFor(() => {
360
- expect(mockNavigateTo).toHaveBeenCalled();
361
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
362
- // Value should be URL encoded
363
- expect(params.profile_filters).toContain('std%3A%3Avector%3Cint%3E');
324
+ expect(onUrlUpdate).toHaveBeenCalled();
325
+ const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
326
+ const filterValue = lastCall.searchParams.get('profile_filters');
327
+ // The value should contain the encoded special characters
328
+ expect(filterValue).toContain('std%3A%3Avector%3Cint%3E');
364
329
  });
365
330
  });
366
331
  it('should filter out incomplete filters when encoding', async () => {
367
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
332
+ const onUrlUpdate = vi.fn();
333
+ const { result } = renderHook(() => useProfileFiltersUrlState(), {
334
+ wrapper: createWrapper({}, onUrlUpdate),
335
+ });
368
336
  const incompleteFilters = [
369
337
  {
370
338
  id: 'complete-1',
@@ -389,10 +357,10 @@ describe('useProfileFiltersUrlState', () => {
389
357
  result.current.setAppliedFilters(incompleteFilters);
390
358
  });
391
359
  await waitFor(() => {
392
- expect(mockNavigateTo).toHaveBeenCalled();
393
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
360
+ expect(onUrlUpdate).toHaveBeenCalled();
361
+ const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
394
362
  // Only the complete filter should be encoded
395
- expect(params.profile_filters).toBe('f:b:!~:valid');
363
+ expect(lastCall.searchParams.get('profile_filters')).toBe('f:b:!~:valid');
396
364
  });
397
365
  });
398
366
  });
@@ -409,8 +377,9 @@ describe('useProfileFiltersUrlState', () => {
409
377
  expect(result.current.appliedFilters).toEqual([]);
410
378
  });
411
379
  it('should return correctly structured filters from URL', async () => {
412
- mockLocation.search = '?profile_filters=s:fn:=:testFunc';
413
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
380
+ const { result } = renderHook(() => useProfileFiltersUrlState(), {
381
+ wrapper: createWrapper({ profile_filters: 's:fn:=:testFunc' }),
382
+ });
414
383
  await waitFor(() => {
415
384
  expect(result.current.appliedFilters).toHaveLength(1);
416
385
  });
@@ -426,13 +395,12 @@ describe('useProfileFiltersUrlState', () => {
426
395
  });
427
396
  describe('View switching scenarios', () => {
428
397
  it('should completely replace filters when switching views using forceApplyFilters', async () => {
429
- // Start with View A's filters (2 filters)
430
- mockLocation.search = '?profile_filters=s:fn:=:viewAFunc,f:b:!=:viewABinary';
431
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
398
+ const onUrlUpdate = vi.fn();
399
+ const { result } = renderHook(() => useProfileFiltersUrlState(), {
400
+ wrapper: createWrapper({ profile_filters: 's:fn:=:viewAFunc,f:b:!=:viewABinary' }, onUrlUpdate),
401
+ });
432
402
  await waitFor(() => {
433
403
  expect(result.current.appliedFilters).toHaveLength(2);
434
- expect(result.current.appliedFilters[0].value).toBe('viewAFunc');
435
- expect(result.current.appliedFilters[1].value).toBe('viewABinary');
436
404
  });
437
405
  // Switch to View B (completely different filter)
438
406
  const viewBFilters = [
@@ -448,77 +416,23 @@ describe('useProfileFiltersUrlState', () => {
448
416
  result.current.forceApplyFilters(viewBFilters);
449
417
  });
450
418
  await waitFor(() => {
451
- expect(mockNavigateTo).toHaveBeenCalled();
452
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
419
+ expect(onUrlUpdate).toHaveBeenCalled();
420
+ const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
421
+ const filterValue = lastCall.searchParams.get('profile_filters');
453
422
  // View A's filters should be completely gone
454
- expect(params.profile_filters).not.toContain('viewAFunc');
455
- expect(params.profile_filters).not.toContain('viewABinary');
423
+ expect(filterValue).not.toContain('viewAFunc');
424
+ expect(filterValue).not.toContain('viewABinary');
456
425
  // Only View B's filter should be present
457
- expect(params.profile_filters).toBe('f:fn:~:viewBOnly');
458
- });
459
- });
460
- it('should handle sequential view switches correctly', async () => {
461
- // Simulate: [default] -> [storage] -> [testing-view]
462
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
463
- // View 1: default view (1 filter)
464
- const defaultFilters = [{ id: 'd-1', type: 'hide_libc', value: 'enabled' }];
465
- act(() => {
466
- result.current.forceApplyFilters(defaultFilters);
467
- });
468
- await waitFor(() => {
469
- expect(mockNavigateTo).toHaveBeenCalled();
470
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
471
- expect(params.profile_filters).toBe('p:hide_libc:enabled');
472
- });
473
- mockNavigateTo.mockClear();
474
- // View 2: storage view (3 filters)
475
- const storageFilters = [
476
- { id: 's-1', type: 'stack', field: 'function_name', matchType: 'not_contains', value: 'io' },
477
- { id: 's-2', type: 'frame', field: 'binary', matchType: 'not_contains', value: 'disk' },
478
- { id: 's-3', type: 'frame', field: 'function_name', matchType: 'contains', value: 'storage' },
479
- ];
480
- act(() => {
481
- result.current.forceApplyFilters(storageFilters);
482
- });
483
- await waitFor(() => {
484
- expect(mockNavigateTo).toHaveBeenCalled();
485
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
486
- // Default view's filter should be gone
487
- expect(params.profile_filters).not.toContain('hide_libc');
488
- // Storage view should have 3 filters
489
- expect(params.profile_filters).toContain('io');
490
- expect(params.profile_filters).toContain('disk');
491
- expect(params.profile_filters).toContain('storage');
492
- });
493
- mockNavigateTo.mockClear();
494
- // View 3: testing-view (2 filters)
495
- const testingFilters = [
496
- { id: 't-1', type: 'stack', field: 'function_name', matchType: 'equal', value: 'test_main' },
497
- { id: 't-2', type: 'frame', field: 'binary', matchType: 'contains', value: 'test' },
498
- ];
499
- act(() => {
500
- result.current.forceApplyFilters(testingFilters);
501
- });
502
- await waitFor(() => {
503
- expect(mockNavigateTo).toHaveBeenCalled();
504
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
505
- // Storage view's filters should be gone
506
- expect(params.profile_filters).not.toContain('io');
507
- expect(params.profile_filters).not.toContain('disk');
508
- expect(params.profile_filters).not.toContain('storage');
509
- // Testing view should have its 2 filters
510
- expect(params.profile_filters).toContain('test_main');
511
- expect(params.profile_filters).toContain('test');
426
+ expect(filterValue).toBe('f:fn:~:viewBOnly');
512
427
  });
513
428
  });
514
429
  it('should not change filters when clicking the same view tab', async () => {
515
- // Start with existing filters
516
- mockLocation.search = '?profile_filters=s:fn:=:existingFilter';
517
- const { result } = renderHook(() => useProfileFiltersUrlState(), { wrapper: createWrapper() });
430
+ const { result } = renderHook(() => useProfileFiltersUrlState(), {
431
+ wrapper: createWrapper({ profile_filters: 's:fn:=:existingFilter' }),
432
+ });
518
433
  await waitFor(() => {
519
434
  expect(result.current.appliedFilters).toHaveLength(1);
520
435
  });
521
- mockNavigateTo.mockClear();
522
436
  // Apply the same filters (simulating clicking the same view tab)
523
437
  const sameFilters = [
524
438
  {
@@ -1 +1 @@
1
- {"version":3,"file":"MultiLevelDropdown.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAQtE,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAkK1C,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACnC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,cAAc,EAAE,OAAO,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oBAAoB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED,QAAA,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAsPzD,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"MultiLevelDropdown.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAQtE,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAmK1C,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACnC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,cAAc,EAAE,OAAO,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oBAAoB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED,QAAA,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAoPzD,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
@@ -15,9 +15,10 @@ import { useCallback, useEffect, useRef, useState } from 'react';
15
15
  import { Menu } from '@headlessui/react';
16
16
  import { Icon } from '@iconify/react';
17
17
  import cx from 'classnames';
18
- import { useURLState } from '@parca/components';
18
+ import { useQueryState } from 'nuqs';
19
19
  import { USER_PREFERENCES, useUserPreference } from '@parca/hooks';
20
20
  import { FIELD_FUNCTION_FILE_NAME, FIELD_FUNCTION_NAME, FIELD_LOCATION_ADDRESS, FIELD_MAPPING_FILE, } from '../../../ProfileFlameGraph/FlameGraphArrow';
21
+ import { boolParam, hiddenBinariesParser, stringParam } from '../../../hooks/urlParsers';
21
22
  import { useProfileViewContext } from '../../context/ProfileViewContext';
22
23
  import SwitchMenuItem from './SwitchMenuItem';
23
24
  const MenuItem = ({ label, items, onclick, onSelect, path = [], id, closeDropdown, isNested = false, activeValueForSortBy, activeValueForColorBy, activeValuesForLevel, value, disabled = false, icon, customSubmenu, renderAsDiv = false, }) => {
@@ -73,23 +74,18 @@ const MenuItem = ({ label, items, onclick, onSelect, path = [], id, closeDropdow
73
74
  const MultiLevelDropdown = ({ onSelect, profileType, groupBy, toggleGroupBy, isTableVizOnly, alignFunctionName, setAlignFunctionName, colorBy, setColorBy, }) => {
74
75
  const dropdownRef = useRef(null);
75
76
  const [shouldOpenLeft, setShouldOpenLeft] = useState(false);
76
- const [storeSortBy] = useURLState('sort_by', {
77
- defaultValue: FIELD_FUNCTION_NAME,
78
- });
79
- const [colorStackLegend, setStoreColorStackLegend] = useURLState('color_stack_legend');
80
- const [hiddenBinaries, setHiddenBinaries] = useURLState('hidden_binaries', {
81
- defaultValue: [],
82
- alwaysReturnArray: true,
83
- });
77
+ const [storeSortBy] = useQueryState('sort_by', stringParam.withDefault(FIELD_FUNCTION_NAME));
78
+ const [colorStackLegend, setStoreColorStackLegend] = useQueryState('color_stack_legend', stringParam);
79
+ const [hiddenBinaries, setHiddenBinaries] = useQueryState('hidden_binaries', hiddenBinariesParser);
84
80
  const { compareMode } = useProfileViewContext();
85
81
  const [colorProfileName] = useUserPreference(USER_PREFERENCES.FLAMEGRAPH_COLOR_PROFILE.key);
86
82
  const isColorStackLegendEnabled = colorStackLegend === 'true';
87
83
  const isLeftAligned = alignFunctionName === 'left';
88
84
  // By default, we want delta profiles (CPU) to be relatively compared.
89
85
  // For non-delta profiles, like goroutines or memory, we want the profiles to be compared absolutely.
90
- const compareAbsoluteDefault = profileType?.delta === false ? 'true' : 'false';
91
- const [compareAbsolute = compareAbsoluteDefault, setCompareAbsolute] = useURLState('compare_absolute');
92
- const isCompareAbsolute = compareAbsolute === 'true';
86
+ const compareAbsoluteDefault = profileType?.delta === false;
87
+ const [compareAbsolute, setCompareAbsolute] = useQueryState('compare_absolute', boolParam);
88
+ const isCompareAbsolute = compareAbsolute ?? compareAbsoluteDefault;
93
89
  useEffect(() => {
94
90
  const checkOverflow = () => {
95
91
  if (dropdownRef.current !== null) {
@@ -108,13 +104,13 @@ const MultiLevelDropdown = ({ onSelect, profileType, groupBy, toggleGroupBy, isT
108
104
  const handleBinaryToggle = (index) => {
109
105
  const updatedBinaries = [...hiddenBinaries];
110
106
  updatedBinaries.splice(index, 1);
111
- setHiddenBinaries(updatedBinaries);
107
+ void setHiddenBinaries(updatedBinaries);
112
108
  };
113
109
  const setColorStackLegend = useCallback((value) => {
114
- setStoreColorStackLegend(value);
110
+ void setStoreColorStackLegend(value);
115
111
  }, [setStoreColorStackLegend]);
116
112
  const resetLegend = () => {
117
- setHiddenBinaries([]);
113
+ void setHiddenBinaries([]);
118
114
  };
119
115
  const menuItems = [
120
116
  {
@@ -181,7 +177,7 @@ const MultiLevelDropdown = ({ onSelect, profileType, groupBy, toggleGroupBy, isT
181
177
  },
182
178
  {
183
179
  label: isCompareAbsolute ? 'Compare Relative' : 'Compare Absolute',
184
- onclick: () => setCompareAbsolute(isCompareAbsolute ? 'false' : 'true'),
180
+ onclick: () => void setCompareAbsolute(!isCompareAbsolute),
185
181
  hide: !compareMode,
186
182
  icon: isCompareAbsolute ? 'fluent-mdl2:compare' : 'fluent-mdl2:compare-uneven',
187
183
  },
@@ -199,7 +195,7 @@ const MultiLevelDropdown = ({ onSelect, profileType, groupBy, toggleGroupBy, isT
199
195
  },
200
196
  {
201
197
  label: 'Reset Legend',
202
- hide: hiddenBinaries === undefined || hiddenBinaries.length === 0,
198
+ hide: hiddenBinaries.length === 0,
203
199
  onclick: () => resetLegend(),
204
200
  id: 'h-reset-legend-button',
205
201
  icon: 'system-uicons:reset',
@@ -207,16 +203,16 @@ const MultiLevelDropdown = ({ onSelect, profileType, groupBy, toggleGroupBy, isT
207
203
  {
208
204
  label: 'Hidden Binaries',
209
205
  id: 'h-hidden-binaries',
210
- items: hiddenBinaries?.map((binary, index) => ({
206
+ items: hiddenBinaries.map((binary, index) => ({
211
207
  label: binary,
212
208
  customSubmenu: (_jsxs("div", { className: "flex items-center gap-2 w-full", children: [_jsx("input", { id: binary, name: binary, type: "checkbox", className: "h-4 w-4 rounded-md border-2 border-gray-300 text-indigo-600 focus:ring-indigo-600 focus:ring-offset-0 checked:bg-indigo-600 checked:border-indigo-600", checked: hiddenBinaries?.includes(binary), onChange: () => handleBinaryToggle(index) }), _jsx("span", { children: binary })] })),
213
209
  })),
214
- hide: hiddenBinaries === undefined || hiddenBinaries.length === 0,
210
+ hide: hiddenBinaries.length === 0,
215
211
  icon: 'ph:eye-closed',
216
212
  },
217
213
  ];
218
214
  return (_jsx("div", { className: "relative inline-block text-left", id: "h-visualisation-toolbar-actions", ref: dropdownRef, children: _jsx(Menu, { children: ({ open, close }) => (_jsxs(_Fragment, { children: [_jsxs(Menu.Button, { className: "flex dark:bg-gray-900 dark:border-gray-600 justify-center w-full px-4 py-2 text-sm font-normal text-gray-600 dark:text-gray-200 bg-white rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 border border-gray-200 pr-[1.7rem]", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { icon: "pajamas:preferences", className: "w-4 h-4" }), _jsx("span", { children: "Preferences" })] }), _jsx("span", { className: "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2 text-gray-400", children: _jsx(Icon, { icon: "heroicons:chevron-down-20-solid", "aria-hidden": "true" }) })] }), open && (_jsx(Menu.Items, { className: cx(isTableVizOnly ? 'w-64' : 'w-80', 'absolute z-50 mt-2 py-2 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none border dark:bg-gray-900 dark:border-gray-600', shouldOpenLeft ? 'right-0 origin-top-right' : 'left-0 origin-top-left'), children: menuItems
219
215
  .filter(item => item.hide !== undefined && !item.hide)
220
- .map((item, index) => (_jsx(MenuItem, { ...item, onSelect: onSelect, closeDropdown: close, activeValueForSortBy: storeSortBy, activeValueForColorBy: colorBy === undefined || colorBy === '' ? 'binary' : colorBy, activeValuesForLevel: groupBy, renderAsDiv: item.renderAsDiv }, index))) }))] })) }) }));
216
+ .map((item, index) => (_jsx(MenuItem, { ...item, onSelect: onSelect, closeDropdown: close, activeValueForSortBy: storeSortBy, activeValueForColorBy: colorBy, activeValuesForLevel: groupBy, renderAsDiv: item.renderAsDiv }, index))) }))] })) }) }));
221
217
  };
222
218
  export default MultiLevelDropdown;
@@ -1 +1 @@
1
- {"version":3,"file":"TableColumnsDropdown.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/Toolbars/TableColumnsDropdown.tsx"],"names":[],"mappings":"AAkBA,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAQ1C,UAAU,KAAK;IACb,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,QAAA,MAAM,oBAAoB,GAAI,kCAAgC,KAAK,KAAG,GAAG,CAAC,OA+KzE,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"TableColumnsDropdown.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/Toolbars/TableColumnsDropdown.tsx"],"names":[],"mappings":"AAkBA,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAS1C,UAAU,KAAK;IACb,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,QAAA,MAAM,oBAAoB,GAAI,kCAAgC,KAAK,KAAG,GAAG,CAAC,OA6KzE,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
@@ -13,16 +13,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  // limitations under the License.
14
14
  import { useEffect, useMemo, useState } from 'react';
15
15
  import { createColumnHelper } from '@tanstack/table-core';
16
- import { useURLState } from '@parca/components';
16
+ import { useQueryState } from 'nuqs';
17
17
  import { valueFormatter } from '@parca/utilities';
18
18
  import ColumnsVisibility from '../../../Table/ColumnsVisibility';
19
19
  import { addPlusSign, getRatioString } from '../../../Table/utils/functions';
20
+ import { tableColumnsParser } from '../../../hooks/urlParsers';
20
21
  import { useProfileViewContext } from '../../context/ProfileViewContext';
21
22
  const TableColumnsDropdown = ({ profileType, total, filtered }) => {
22
23
  const { compareMode } = useProfileViewContext();
23
- const [tableColumns, setTableColumns] = useURLState('table_columns', {
24
- alwaysReturnArray: true,
25
- });
24
+ const [tableColumns, setTableColumns] = useQueryState('table_columns', tableColumnsParser);
26
25
  const columnHelper = createColumnHelper();
27
26
  const unit = useMemo(() => profileType?.sampleUnit ?? '', [profileType?.sampleUnit]);
28
27
  const columns = useMemo(() => {
@@ -169,7 +168,7 @@ const TableColumnsDropdown = ({ profileType, total, filtered }) => {
169
168
  const updateColumnVisibility = (column, isVisible) => {
170
169
  const updatedColumns = { ...columnVisibility, [column]: isVisible };
171
170
  const newTableColumns = Object.keys(updatedColumns).filter(col => updatedColumns[col]);
172
- setTableColumns(newTableColumns);
171
+ void setTableColumns(newTableColumns);
173
172
  };
174
173
  return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-sm", children: "Table Columns" }), _jsx(ColumnsVisibility, { columns: columns, visibility: columnVisibility, setVisibility: (id, visible) => {
175
174
  updateColumnVisibility(id, visible);