@parca/profile 0.19.138 → 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 (138) hide show
  1. package/CHANGELOG.md +10 -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/ProfileSelector/useAutoQuerySelector.d.ts.map +1 -1
  23. package/dist/ProfileSelector/useAutoQuerySelector.js +3 -0
  24. package/dist/ProfileTypeSelector/index.d.ts.map +1 -1
  25. package/dist/ProfileTypeSelector/index.js +4 -0
  26. package/dist/ProfileView/components/ActionButtons/SortByDropdown.d.ts.map +1 -1
  27. package/dist/ProfileView/components/ActionButtons/SortByDropdown.js +5 -5
  28. package/dist/ProfileView/components/ColorStackLegend.d.ts.map +1 -1
  29. package/dist/ProfileView/components/ColorStackLegend.js +2 -3
  30. package/dist/ProfileView/components/InvertCallStack/index.d.ts.map +1 -1
  31. package/dist/ProfileView/components/InvertCallStack/index.js +5 -4
  32. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts +1 -2
  33. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
  34. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +14 -16
  35. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.js +84 -170
  36. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts.map +1 -1
  37. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +16 -20
  38. package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.d.ts.map +1 -1
  39. package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.js +4 -5
  40. package/dist/ProfileView/components/Toolbars/index.d.ts +2 -2
  41. package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
  42. package/dist/ProfileView/components/Toolbars/index.js +1 -1
  43. package/dist/ProfileView/components/ViewSelector/index.d.ts.map +1 -1
  44. package/dist/ProfileView/components/ViewSelector/index.js +8 -14
  45. package/dist/ProfileView/context/DashboardContext.d.ts.map +1 -1
  46. package/dist/ProfileView/context/DashboardContext.js +6 -6
  47. package/dist/ProfileView/hooks/useResetFlameGraphState.d.ts.map +1 -1
  48. package/dist/ProfileView/hooks/useResetFlameGraphState.js +5 -4
  49. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.d.ts.map +1 -1
  50. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.js +25 -26
  51. package/dist/ProfileView/hooks/useResetStateOnSeriesChange.d.ts.map +1 -1
  52. package/dist/ProfileView/hooks/useResetStateOnSeriesChange.js +13 -8
  53. package/dist/ProfileView/hooks/useVisualizationState.d.ts +3 -3
  54. package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
  55. package/dist/ProfileView/hooks/useVisualizationState.js +35 -51
  56. package/dist/ProfileViewWithData.d.ts.map +1 -1
  57. package/dist/ProfileViewWithData.js +19 -28
  58. package/dist/Sandwich/index.d.ts.map +1 -1
  59. package/dist/Sandwich/index.js +4 -3
  60. package/dist/SourceView/index.d.ts.map +1 -1
  61. package/dist/SourceView/index.js +4 -2
  62. package/dist/SourceView/useSelectedLineRange.d.ts.map +1 -1
  63. package/dist/SourceView/useSelectedLineRange.js +21 -16
  64. package/dist/Table/MoreDropdown.d.ts.map +1 -1
  65. package/dist/Table/MoreDropdown.js +8 -11
  66. package/dist/Table/TableContextMenu.d.ts.map +1 -1
  67. package/dist/Table/TableContextMenu.js +10 -13
  68. package/dist/Table/hooks/useTableConfiguration.d.ts.map +1 -1
  69. package/dist/Table/hooks/useTableConfiguration.js +3 -4
  70. package/dist/Table/index.d.ts.map +1 -1
  71. package/dist/Table/index.js +11 -9
  72. package/dist/TopTable/index.d.ts.map +1 -1
  73. package/dist/TopTable/index.js +3 -4
  74. package/dist/hooks/urlParsers.d.ts +18 -0
  75. package/dist/hooks/urlParsers.d.ts.map +1 -0
  76. package/dist/hooks/urlParsers.js +32 -0
  77. package/dist/hooks/useColorBy.d.ts +5 -0
  78. package/dist/hooks/useColorBy.d.ts.map +1 -0
  79. package/dist/hooks/useColorBy.js +26 -0
  80. package/dist/hooks/useCompareModeMeta.d.ts.map +1 -1
  81. package/dist/hooks/useCompareModeMeta.js +55 -86
  82. package/dist/hooks/useDashboardItems.d.ts +5 -0
  83. package/dist/hooks/useDashboardItems.d.ts.map +1 -0
  84. package/dist/hooks/useDashboardItems.js +27 -0
  85. package/dist/hooks/useQueryState.d.ts +3 -3
  86. package/dist/hooks/useQueryState.d.ts.map +1 -1
  87. package/dist/hooks/useQueryState.js +105 -105
  88. package/dist/hooks/useQueryState.test.js +186 -302
  89. package/dist/index.d.ts +3 -2
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +3 -12
  92. package/dist/useSumBy.d.ts +1 -1
  93. package/dist/useSumBy.d.ts.map +1 -1
  94. package/dist/useSumBy.js +2 -2
  95. package/package.json +12 -11
  96. package/src/GraphTooltipArrow/useGraphTooltipMetaInfo/index.ts +11 -13
  97. package/src/ProfileExplorer/ProfileExplorerCompare.tsx +4 -9
  98. package/src/ProfileFlameChart/SamplesStrips/index.tsx +2 -2
  99. package/src/ProfileFlameChart/index.tsx +21 -28
  100. package/src/ProfileFlameGraph/FlameGraphArrow/ContextMenu.tsx +10 -9
  101. package/src/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.tsx +5 -3
  102. package/src/ProfileFlameGraph/index.tsx +6 -9
  103. package/src/ProfileMetricsGraph/index.tsx +6 -8
  104. package/src/ProfileSelector/MetricsGraphSection.tsx +5 -10
  105. package/src/ProfileSelector/index.tsx +32 -31
  106. package/src/ProfileSelector/useAutoQuerySelector.ts +5 -0
  107. package/src/ProfileTypeSelector/index.tsx +4 -0
  108. package/src/ProfileView/components/ActionButtons/SortByDropdown.tsx +10 -6
  109. package/src/ProfileView/components/ColorStackLegend.tsx +2 -4
  110. package/src/ProfileView/components/InvertCallStack/index.tsx +5 -4
  111. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.tsx +94 -192
  112. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +21 -21
  113. package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +24 -25
  114. package/src/ProfileView/components/Toolbars/TableColumnsDropdown.tsx +4 -5
  115. package/src/ProfileView/components/Toolbars/index.tsx +3 -3
  116. package/src/ProfileView/components/ViewSelector/index.tsx +9 -16
  117. package/src/ProfileView/context/DashboardContext.tsx +6 -6
  118. package/src/ProfileView/hooks/useResetFlameGraphState.ts +6 -4
  119. package/src/ProfileView/hooks/useResetStateOnProfileTypeChange.ts +24 -26
  120. package/src/ProfileView/hooks/useResetStateOnSeriesChange.ts +16 -8
  121. package/src/ProfileView/hooks/useVisualizationState.ts +61 -69
  122. package/src/ProfileViewWithData.tsx +29 -35
  123. package/src/Sandwich/index.tsx +4 -3
  124. package/src/SourceView/index.tsx +4 -2
  125. package/src/SourceView/useSelectedLineRange.ts +34 -19
  126. package/src/Table/MoreDropdown.tsx +9 -11
  127. package/src/Table/TableContextMenu.tsx +10 -13
  128. package/src/Table/hooks/useTableConfiguration.tsx +3 -4
  129. package/src/Table/index.tsx +12 -21
  130. package/src/TopTable/index.tsx +3 -4
  131. package/src/hooks/urlParsers.ts +38 -0
  132. package/src/hooks/useColorBy.ts +42 -0
  133. package/src/hooks/useCompareModeMeta.ts +61 -91
  134. package/src/hooks/useDashboardItems.ts +46 -0
  135. package/src/hooks/useQueryState.test.tsx +275 -345
  136. package/src/hooks/useQueryState.ts +136 -118
  137. package/src/index.tsx +16 -15
  138. package/src/useSumBy.ts +3 -3
@@ -11,13 +11,13 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
11
11
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
- import { Select, useURLState } from '@parca/components';
14
+ import { useQueryState } from 'nuqs';
15
+ import { Select } from '@parca/components';
15
16
  import { FIELD_CUMULATIVE, FIELD_DIFF, FIELD_FUNCTION_NAME, } from '../../../ProfileFlameGraph/FlameGraphArrow';
17
+ import { stringParam } from '../../../hooks/urlParsers';
16
18
  import { useProfileViewContext } from '../../context/ProfileViewContext';
17
19
  const SortByDropdown = () => {
18
- const [storeSortBy, setStoreSortBy] = useURLState('sort_by', {
19
- defaultValue: FIELD_FUNCTION_NAME,
20
- });
20
+ const [storeSortBy, setStoreSortBy] = useQueryState('sort_by', stringParam.withDefault(FIELD_FUNCTION_NAME));
21
21
  const { compareMode } = useProfileViewContext();
22
22
  return (_jsxs("div", { children: [_jsx("label", { className: "text-sm", children: "Sort by" }), _jsx(Select, { className: "!px-3", items: [
23
23
  {
@@ -44,6 +44,6 @@ const SortByDropdown = () => {
44
44
  expanded: (_jsx(_Fragment, { children: _jsx("span", { children: "Diff" }) })),
45
45
  },
46
46
  },
47
- ], selectedKey: storeSortBy, onSelection: key => setStoreSortBy(key), placeholder: 'Sort By', primary: false, disabled: false, id: "h-sort-by-filter" })] }));
47
+ ], selectedKey: storeSortBy, onSelection: key => void setStoreSortBy(key), placeholder: 'Sort By', primary: false, disabled: false, id: "h-sort-by-filter" })] }));
48
48
  };
49
49
  export default SortByDropdown;
@@ -1 +1 @@
1
- {"version":3,"file":"ColorStackLegend.d.ts","sourceRoot":"","sources":["../../../src/ProfileView/components/ColorStackLegend.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAgB,MAAM,OAAO,CAAC;AAarC,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,QAAA,MAAM,gBAAgB,GAAI,oCAA0C,KAAK,KAAG,KAAK,CAAC,GAAG,CAAC,OAuGrF,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"ColorStackLegend.d.ts","sourceRoot":"","sources":["../../../src/ProfileView/components/ColorStackLegend.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAgB,MAAM,OAAO,CAAC;AAarC,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,QAAA,MAAM,gBAAgB,GAAI,oCAA0C,KAAK,KAAG,KAAK,CAAC,GAAG,CAAC,OAqGrF,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -14,18 +14,17 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
14
14
  import { useMemo } from 'react';
15
15
  import { Icon } from '@iconify/react';
16
16
  import cx from 'classnames';
17
- import { useURLState } from '@parca/components';
18
17
  import { USER_PREFERENCES, useCurrentColorProfile, useUserPreference } from '@parca/hooks';
19
18
  import { EVERYTHING_ELSE, selectDarkMode, useAppSelector } from '@parca/store';
20
19
  import { getMappingColors } from '../../ProfileFlameGraph/FlameGraphArrow';
21
20
  import useMappingList from '../../ProfileFlameGraph/FlameGraphArrow/useMappingList';
21
+ import { useColorBy } from '../../hooks/useColorBy';
22
22
  import { useProfileFilters } from './ProfileFilters/useProfileFilters';
23
23
  const ColorStackLegend = ({ mappings, compareMode = false, loading }) => {
24
24
  const isDarkMode = useAppSelector(selectDarkMode);
25
25
  const currentColorProfile = useCurrentColorProfile();
26
26
  const [colorProfileName] = useUserPreference(USER_PREFERENCES.FLAMEGRAPH_COLOR_PROFILE.key);
27
- const [colorByValue, _] = useURLState('color_by');
28
- const colorBy = colorByValue === 'binary' || colorByValue === undefined ? 'binary' : 'filename';
27
+ const { colorBy } = useColorBy();
29
28
  const { appliedFilters, removeExcludeBinary, excludeBinary } = useProfileFilters();
30
29
  // Get current binary filters from the new ProfileFilters system
31
30
  const currentBinaryFilters = useMemo(() => {
@@ -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"}