@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.
- package/CHANGELOG.md +4 -0
- package/dist/GraphTooltipArrow/useGraphTooltipMetaInfo/index.d.ts.map +1 -1
- package/dist/GraphTooltipArrow/useGraphTooltipMetaInfo/index.js +11 -13
- package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts.map +1 -1
- package/dist/ProfileExplorer/ProfileExplorerCompare.js +4 -9
- package/dist/ProfileFlameChart/SamplesStrips/index.d.ts +2 -2
- package/dist/ProfileFlameChart/SamplesStrips/index.d.ts.map +1 -1
- package/dist/ProfileFlameChart/index.d.ts.map +1 -1
- package/dist/ProfileFlameChart/index.js +13 -19
- package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.js +8 -8
- package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.js +4 -3
- package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/index.js +6 -4
- package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
- package/dist/ProfileMetricsGraph/index.js +4 -6
- package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
- package/dist/ProfileSelector/MetricsGraphSection.js +5 -10
- package/dist/ProfileSelector/index.d.ts.map +1 -1
- package/dist/ProfileSelector/index.js +27 -25
- package/dist/ProfileView/components/ActionButtons/SortByDropdown.d.ts.map +1 -1
- package/dist/ProfileView/components/ActionButtons/SortByDropdown.js +5 -5
- package/dist/ProfileView/components/ColorStackLegend.d.ts.map +1 -1
- package/dist/ProfileView/components/ColorStackLegend.js +2 -3
- package/dist/ProfileView/components/InvertCallStack/index.d.ts.map +1 -1
- package/dist/ProfileView/components/InvertCallStack/index.js +5 -4
- package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts +1 -2
- package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
- package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +14 -16
- package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.js +84 -170
- package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts.map +1 -1
- package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +16 -20
- package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.d.ts.map +1 -1
- package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.js +4 -5
- package/dist/ProfileView/components/Toolbars/index.d.ts +2 -2
- package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
- package/dist/ProfileView/components/Toolbars/index.js +1 -1
- package/dist/ProfileView/components/ViewSelector/index.d.ts.map +1 -1
- package/dist/ProfileView/components/ViewSelector/index.js +8 -14
- package/dist/ProfileView/context/DashboardContext.d.ts.map +1 -1
- package/dist/ProfileView/context/DashboardContext.js +6 -6
- package/dist/ProfileView/hooks/useResetFlameGraphState.d.ts.map +1 -1
- package/dist/ProfileView/hooks/useResetFlameGraphState.js +5 -4
- package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.d.ts.map +1 -1
- package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.js +25 -26
- package/dist/ProfileView/hooks/useResetStateOnSeriesChange.d.ts.map +1 -1
- package/dist/ProfileView/hooks/useResetStateOnSeriesChange.js +13 -8
- package/dist/ProfileView/hooks/useVisualizationState.d.ts +3 -3
- package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
- package/dist/ProfileView/hooks/useVisualizationState.js +35 -51
- package/dist/ProfileViewWithData.d.ts.map +1 -1
- package/dist/ProfileViewWithData.js +19 -28
- package/dist/Sandwich/index.d.ts.map +1 -1
- package/dist/Sandwich/index.js +4 -3
- package/dist/SourceView/index.d.ts.map +1 -1
- package/dist/SourceView/index.js +4 -2
- package/dist/SourceView/useSelectedLineRange.d.ts.map +1 -1
- package/dist/SourceView/useSelectedLineRange.js +21 -16
- package/dist/Table/MoreDropdown.d.ts.map +1 -1
- package/dist/Table/MoreDropdown.js +8 -11
- package/dist/Table/TableContextMenu.d.ts.map +1 -1
- package/dist/Table/TableContextMenu.js +10 -13
- package/dist/Table/hooks/useTableConfiguration.d.ts.map +1 -1
- package/dist/Table/hooks/useTableConfiguration.js +3 -4
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +11 -9
- package/dist/TopTable/index.d.ts.map +1 -1
- package/dist/TopTable/index.js +3 -4
- package/dist/hooks/urlParsers.d.ts +18 -0
- package/dist/hooks/urlParsers.d.ts.map +1 -0
- package/dist/hooks/urlParsers.js +32 -0
- package/dist/hooks/useColorBy.d.ts +5 -0
- package/dist/hooks/useColorBy.d.ts.map +1 -0
- package/dist/hooks/useColorBy.js +26 -0
- package/dist/hooks/useCompareModeMeta.d.ts.map +1 -1
- package/dist/hooks/useCompareModeMeta.js +55 -86
- package/dist/hooks/useDashboardItems.d.ts +5 -0
- package/dist/hooks/useDashboardItems.d.ts.map +1 -0
- package/dist/hooks/useDashboardItems.js +27 -0
- package/dist/hooks/useQueryState.d.ts +3 -3
- package/dist/hooks/useQueryState.d.ts.map +1 -1
- package/dist/hooks/useQueryState.js +105 -105
- package/dist/hooks/useQueryState.test.js +186 -302
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -12
- package/dist/useSumBy.d.ts +1 -1
- package/dist/useSumBy.d.ts.map +1 -1
- package/dist/useSumBy.js +2 -2
- package/package.json +8 -7
- package/src/GraphTooltipArrow/useGraphTooltipMetaInfo/index.ts +11 -13
- package/src/ProfileExplorer/ProfileExplorerCompare.tsx +4 -9
- package/src/ProfileFlameChart/SamplesStrips/index.tsx +2 -2
- package/src/ProfileFlameChart/index.tsx +21 -28
- package/src/ProfileFlameGraph/FlameGraphArrow/ContextMenu.tsx +10 -9
- package/src/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.tsx +5 -3
- package/src/ProfileFlameGraph/index.tsx +6 -9
- package/src/ProfileMetricsGraph/index.tsx +6 -8
- package/src/ProfileSelector/MetricsGraphSection.tsx +5 -10
- package/src/ProfileSelector/index.tsx +32 -31
- package/src/ProfileView/components/ActionButtons/SortByDropdown.tsx +10 -6
- package/src/ProfileView/components/ColorStackLegend.tsx +2 -4
- package/src/ProfileView/components/InvertCallStack/index.tsx +5 -4
- package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.tsx +94 -192
- package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +21 -21
- package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +24 -25
- package/src/ProfileView/components/Toolbars/TableColumnsDropdown.tsx +4 -5
- package/src/ProfileView/components/Toolbars/index.tsx +3 -3
- package/src/ProfileView/components/ViewSelector/index.tsx +9 -16
- package/src/ProfileView/context/DashboardContext.tsx +6 -6
- package/src/ProfileView/hooks/useResetFlameGraphState.ts +6 -4
- package/src/ProfileView/hooks/useResetStateOnProfileTypeChange.ts +24 -26
- package/src/ProfileView/hooks/useResetStateOnSeriesChange.ts +16 -8
- package/src/ProfileView/hooks/useVisualizationState.ts +61 -69
- package/src/ProfileViewWithData.tsx +29 -35
- package/src/Sandwich/index.tsx +4 -3
- package/src/SourceView/index.tsx +4 -2
- package/src/SourceView/useSelectedLineRange.ts +34 -19
- package/src/Table/MoreDropdown.tsx +9 -11
- package/src/Table/TableContextMenu.tsx +10 -13
- package/src/Table/hooks/useTableConfiguration.tsx +3 -4
- package/src/Table/index.tsx +12 -21
- package/src/TopTable/index.tsx +3 -4
- package/src/hooks/urlParsers.ts +38 -0
- package/src/hooks/useColorBy.ts +42 -0
- package/src/hooks/useCompareModeMeta.ts +61 -91
- package/src/hooks/useDashboardItems.ts +46 -0
- package/src/hooks/useQueryState.test.tsx +275 -345
- package/src/hooks/useQueryState.ts +136 -118
- package/src/index.tsx +16 -15
- package/src/useSumBy.ts +3 -3
|
@@ -11,19 +11,23 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import {useQueryState} from 'nuqs';
|
|
15
|
+
|
|
16
|
+
import {Select} from '@parca/components';
|
|
15
17
|
|
|
16
18
|
import {
|
|
17
19
|
FIELD_CUMULATIVE,
|
|
18
20
|
FIELD_DIFF,
|
|
19
21
|
FIELD_FUNCTION_NAME,
|
|
20
22
|
} from '../../../ProfileFlameGraph/FlameGraphArrow';
|
|
23
|
+
import {stringParam} from '../../../hooks/urlParsers';
|
|
21
24
|
import {useProfileViewContext} from '../../context/ProfileViewContext';
|
|
22
25
|
|
|
23
26
|
const SortByDropdown = (): React.JSX.Element => {
|
|
24
|
-
const [storeSortBy, setStoreSortBy] =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const [storeSortBy, setStoreSortBy] = useQueryState(
|
|
28
|
+
'sort_by',
|
|
29
|
+
stringParam.withDefault(FIELD_FUNCTION_NAME)
|
|
30
|
+
);
|
|
27
31
|
|
|
28
32
|
const {compareMode} = useProfileViewContext();
|
|
29
33
|
|
|
@@ -70,8 +74,8 @@ const SortByDropdown = (): React.JSX.Element => {
|
|
|
70
74
|
},
|
|
71
75
|
},
|
|
72
76
|
]}
|
|
73
|
-
selectedKey={storeSortBy
|
|
74
|
-
onSelection={key => setStoreSortBy(key)}
|
|
77
|
+
selectedKey={storeSortBy}
|
|
78
|
+
onSelection={key => void setStoreSortBy(key)}
|
|
75
79
|
placeholder={'Sort By'}
|
|
76
80
|
primary={false}
|
|
77
81
|
disabled={false}
|
|
@@ -16,12 +16,12 @@ import React, {useMemo} from 'react';
|
|
|
16
16
|
import {Icon} from '@iconify/react';
|
|
17
17
|
import cx from 'classnames';
|
|
18
18
|
|
|
19
|
-
import {useURLState} from '@parca/components';
|
|
20
19
|
import {USER_PREFERENCES, useCurrentColorProfile, useUserPreference} from '@parca/hooks';
|
|
21
20
|
import {EVERYTHING_ELSE, selectDarkMode, useAppSelector} from '@parca/store';
|
|
22
21
|
|
|
23
22
|
import {getMappingColors} from '../../ProfileFlameGraph/FlameGraphArrow';
|
|
24
23
|
import useMappingList from '../../ProfileFlameGraph/FlameGraphArrow/useMappingList';
|
|
24
|
+
import {useColorBy} from '../../hooks/useColorBy';
|
|
25
25
|
import {useProfileFilters} from './ProfileFilters/useProfileFilters';
|
|
26
26
|
|
|
27
27
|
interface Props {
|
|
@@ -37,9 +37,7 @@ const ColorStackLegend = ({mappings, compareMode = false, loading}: Props): Reac
|
|
|
37
37
|
USER_PREFERENCES.FLAMEGRAPH_COLOR_PROFILE.key
|
|
38
38
|
);
|
|
39
39
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const colorBy = colorByValue === 'binary' || colorByValue === undefined ? 'binary' : 'filename';
|
|
40
|
+
const {colorBy} = useColorBy();
|
|
43
41
|
|
|
44
42
|
const {appliedFilters, removeExcludeBinary, excludeBinary} = useProfileFilters();
|
|
45
43
|
|
|
@@ -12,19 +12,20 @@
|
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
14
|
import {Icon} from '@iconify/react';
|
|
15
|
+
import {useQueryState} from 'nuqs';
|
|
15
16
|
|
|
16
|
-
import {Button
|
|
17
|
+
import {Button} from '@parca/components';
|
|
17
18
|
import {TEST_IDS, testId} from '@parca/test-utils';
|
|
18
19
|
|
|
20
|
+
import {invertCallStackParser} from '../../../hooks/urlParsers';
|
|
19
21
|
import {useResetFlameGraphState} from '../../hooks/useResetFlameGraphState';
|
|
20
22
|
|
|
21
23
|
const InvertCallStack = (): JSX.Element => {
|
|
22
|
-
const [
|
|
23
|
-
const isInvert = invertStack === 'true';
|
|
24
|
+
const [isInvert, setInvertStack] = useQueryState('invert_call_stack', invertCallStackParser);
|
|
24
25
|
const resetFlameGraphState = useResetFlameGraphState();
|
|
25
26
|
|
|
26
27
|
const handleSetInvert = (value: boolean): void => {
|
|
27
|
-
setInvertStack(value
|
|
28
|
+
void setInvertStack(value);
|
|
28
29
|
|
|
29
30
|
resetFlameGraphState();
|
|
30
31
|
};
|
|
@@ -15,78 +15,28 @@ 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
|
-
|
|
19
|
-
|
|
20
|
-
import {
|
|
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';
|
|
21
21
|
|
|
22
22
|
import {type ProfileFilter} from './useProfileFilters';
|
|
23
23
|
import {decodeProfileFilters, useProfileFiltersUrlState} from './useProfileFiltersUrlState';
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
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) => {
|
|
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) => {
|
|
73
30
|
const Wrapper = ({children}: {children: ReactNode}): JSX.Element => (
|
|
74
|
-
<
|
|
31
|
+
<NuqsTestingAdapter searchParams={searchParams} onUrlUpdate={onUrlUpdate} hasMemory={true}>
|
|
32
|
+
{children}
|
|
33
|
+
</NuqsTestingAdapter>
|
|
75
34
|
);
|
|
76
|
-
Wrapper.displayName = '
|
|
35
|
+
Wrapper.displayName = 'NuqsTestingWrapper';
|
|
77
36
|
return Wrapper;
|
|
78
37
|
};
|
|
79
38
|
|
|
80
39
|
describe('useProfileFiltersUrlState', () => {
|
|
81
|
-
beforeEach(() => {
|
|
82
|
-
mockNavigateTo.mockClear();
|
|
83
|
-
Object.defineProperty(window, 'location', {
|
|
84
|
-
value: mockLocation,
|
|
85
|
-
writable: true,
|
|
86
|
-
});
|
|
87
|
-
mockLocation.search = '';
|
|
88
|
-
});
|
|
89
|
-
|
|
90
40
|
describe('decodeProfileFilters', () => {
|
|
91
41
|
it('should return empty array for empty string', () => {
|
|
92
42
|
expect(decodeProfileFilters('')).toEqual([]);
|
|
@@ -255,9 +205,9 @@ describe('useProfileFiltersUrlState', () => {
|
|
|
255
205
|
});
|
|
256
206
|
|
|
257
207
|
it('should read filters from URL', async () => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
208
|
+
const {result} = renderHook(() => useProfileFiltersUrlState(), {
|
|
209
|
+
wrapper: createWrapper({profile_filters: 's:fn:=:testFunc'}),
|
|
210
|
+
});
|
|
261
211
|
|
|
262
212
|
await waitFor(() => {
|
|
263
213
|
expect(result.current.appliedFilters).toHaveLength(1);
|
|
@@ -271,7 +221,10 @@ describe('useProfileFiltersUrlState', () => {
|
|
|
271
221
|
});
|
|
272
222
|
|
|
273
223
|
it('should update URL when setting filters', async () => {
|
|
274
|
-
const
|
|
224
|
+
const onUrlUpdate = vi.fn();
|
|
225
|
+
const {result} = renderHook(() => useProfileFiltersUrlState(), {
|
|
226
|
+
wrapper: createWrapper({}, onUrlUpdate),
|
|
227
|
+
});
|
|
275
228
|
|
|
276
229
|
const newFilters: ProfileFilter[] = [
|
|
277
230
|
{
|
|
@@ -288,26 +241,26 @@ describe('useProfileFiltersUrlState', () => {
|
|
|
288
241
|
});
|
|
289
242
|
|
|
290
243
|
await waitFor(() => {
|
|
291
|
-
expect(
|
|
292
|
-
const
|
|
293
|
-
expect(
|
|
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');
|
|
294
247
|
});
|
|
295
248
|
});
|
|
296
249
|
|
|
297
250
|
it('should clear URL param when setting empty filters', async () => {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
251
|
+
const onUrlUpdate = vi.fn();
|
|
252
|
+
const {result} = renderHook(() => useProfileFiltersUrlState(), {
|
|
253
|
+
wrapper: createWrapper({profile_filters: 's:fn:=:testFunc'}, onUrlUpdate),
|
|
254
|
+
});
|
|
301
255
|
|
|
302
256
|
act(() => {
|
|
303
257
|
result.current.setAppliedFilters([]);
|
|
304
258
|
});
|
|
305
259
|
|
|
306
260
|
await waitFor(() => {
|
|
307
|
-
expect(
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
expect(params.profile_filters === '' || params.profile_filters === undefined).toBe(true);
|
|
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);
|
|
311
264
|
});
|
|
312
265
|
});
|
|
313
266
|
});
|
|
@@ -320,9 +273,10 @@ describe('useProfileFiltersUrlState', () => {
|
|
|
320
273
|
});
|
|
321
274
|
|
|
322
275
|
it('should force apply filters overwriting existing', async () => {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
276
|
+
const onUrlUpdate = vi.fn();
|
|
277
|
+
const {result} = renderHook(() => useProfileFiltersUrlState(), {
|
|
278
|
+
wrapper: createWrapper({profile_filters: 's:fn:=:existingFunc'}, onUrlUpdate),
|
|
279
|
+
});
|
|
326
280
|
|
|
327
281
|
// Verify existing filter is loaded
|
|
328
282
|
await waitFor(() => {
|
|
@@ -344,33 +298,36 @@ describe('useProfileFiltersUrlState', () => {
|
|
|
344
298
|
});
|
|
345
299
|
|
|
346
300
|
await waitFor(() => {
|
|
347
|
-
expect(
|
|
348
|
-
const
|
|
349
|
-
expect(
|
|
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');
|
|
350
304
|
});
|
|
351
305
|
});
|
|
352
306
|
|
|
353
307
|
it('should clear filters when force applying empty array', async () => {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
308
|
+
const onUrlUpdate = vi.fn();
|
|
309
|
+
const {result} = renderHook(() => useProfileFiltersUrlState(), {
|
|
310
|
+
wrapper: createWrapper({profile_filters: 's:fn:=:existingFunc'}, onUrlUpdate),
|
|
311
|
+
});
|
|
357
312
|
|
|
358
313
|
act(() => {
|
|
359
314
|
result.current.forceApplyFilters([]);
|
|
360
315
|
});
|
|
361
316
|
|
|
362
317
|
await waitFor(() => {
|
|
363
|
-
expect(
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
expect(params.profile_filters === '' || params.profile_filters === undefined).toBe(true);
|
|
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);
|
|
367
321
|
});
|
|
368
322
|
});
|
|
369
323
|
});
|
|
370
324
|
|
|
371
325
|
describe('Preset filter encoding', () => {
|
|
372
326
|
it('should encode preset filters correctly', async () => {
|
|
373
|
-
const
|
|
327
|
+
const onUrlUpdate = vi.fn();
|
|
328
|
+
const {result} = renderHook(() => useProfileFiltersUrlState(), {
|
|
329
|
+
wrapper: createWrapper({}, onUrlUpdate),
|
|
330
|
+
});
|
|
374
331
|
|
|
375
332
|
const presetFilters: ProfileFilter[] = [
|
|
376
333
|
{
|
|
@@ -385,14 +342,17 @@ describe('useProfileFiltersUrlState', () => {
|
|
|
385
342
|
});
|
|
386
343
|
|
|
387
344
|
await waitFor(() => {
|
|
388
|
-
expect(
|
|
389
|
-
const
|
|
390
|
-
expect(
|
|
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');
|
|
391
348
|
});
|
|
392
349
|
});
|
|
393
350
|
|
|
394
351
|
it('should handle mixed preset and regular filters', async () => {
|
|
395
|
-
const
|
|
352
|
+
const onUrlUpdate = vi.fn();
|
|
353
|
+
const {result} = renderHook(() => useProfileFiltersUrlState(), {
|
|
354
|
+
wrapper: createWrapper({}, onUrlUpdate),
|
|
355
|
+
});
|
|
396
356
|
|
|
397
357
|
const mixedFilters: ProfileFilter[] = [
|
|
398
358
|
{
|
|
@@ -414,16 +374,21 @@ describe('useProfileFiltersUrlState', () => {
|
|
|
414
374
|
});
|
|
415
375
|
|
|
416
376
|
await waitFor(() => {
|
|
417
|
-
expect(
|
|
418
|
-
const
|
|
419
|
-
expect(
|
|
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
|
+
);
|
|
420
382
|
});
|
|
421
383
|
});
|
|
422
384
|
});
|
|
423
385
|
|
|
424
386
|
describe('URL encoding edge cases', () => {
|
|
425
387
|
it('should handle special characters in filter values', async () => {
|
|
426
|
-
const
|
|
388
|
+
const onUrlUpdate = vi.fn();
|
|
389
|
+
const {result} = renderHook(() => useProfileFiltersUrlState(), {
|
|
390
|
+
wrapper: createWrapper({}, onUrlUpdate),
|
|
391
|
+
});
|
|
427
392
|
|
|
428
393
|
const filtersWithSpecialChars: ProfileFilter[] = [
|
|
429
394
|
{
|
|
@@ -440,15 +405,19 @@ describe('useProfileFiltersUrlState', () => {
|
|
|
440
405
|
});
|
|
441
406
|
|
|
442
407
|
await waitFor(() => {
|
|
443
|
-
expect(
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
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');
|
|
447
413
|
});
|
|
448
414
|
});
|
|
449
415
|
|
|
450
416
|
it('should filter out incomplete filters when encoding', async () => {
|
|
451
|
-
const
|
|
417
|
+
const onUrlUpdate = vi.fn();
|
|
418
|
+
const {result} = renderHook(() => useProfileFiltersUrlState(), {
|
|
419
|
+
wrapper: createWrapper({}, onUrlUpdate),
|
|
420
|
+
});
|
|
452
421
|
|
|
453
422
|
const incompleteFilters: ProfileFilter[] = [
|
|
454
423
|
{
|
|
@@ -476,10 +445,10 @@ describe('useProfileFiltersUrlState', () => {
|
|
|
476
445
|
});
|
|
477
446
|
|
|
478
447
|
await waitFor(() => {
|
|
479
|
-
expect(
|
|
480
|
-
const
|
|
448
|
+
expect(onUrlUpdate).toHaveBeenCalled();
|
|
449
|
+
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
|
|
481
450
|
// Only the complete filter should be encoded
|
|
482
|
-
expect(
|
|
451
|
+
expect(lastCall.searchParams.get('profile_filters')).toBe('f:b:!~:valid');
|
|
483
452
|
});
|
|
484
453
|
});
|
|
485
454
|
});
|
|
@@ -501,9 +470,9 @@ describe('useProfileFiltersUrlState', () => {
|
|
|
501
470
|
});
|
|
502
471
|
|
|
503
472
|
it('should return correctly structured filters from URL', async () => {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
473
|
+
const {result} = renderHook(() => useProfileFiltersUrlState(), {
|
|
474
|
+
wrapper: createWrapper({profile_filters: 's:fn:=:testFunc'}),
|
|
475
|
+
});
|
|
507
476
|
|
|
508
477
|
await waitFor(() => {
|
|
509
478
|
expect(result.current.appliedFilters).toHaveLength(1);
|
|
@@ -522,15 +491,16 @@ describe('useProfileFiltersUrlState', () => {
|
|
|
522
491
|
|
|
523
492
|
describe('View switching scenarios', () => {
|
|
524
493
|
it('should completely replace filters when switching views using forceApplyFilters', async () => {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
+
});
|
|
529
501
|
|
|
530
502
|
await waitFor(() => {
|
|
531
503
|
expect(result.current.appliedFilters).toHaveLength(2);
|
|
532
|
-
expect(result.current.appliedFilters[0].value).toBe('viewAFunc');
|
|
533
|
-
expect(result.current.appliedFilters[1].value).toBe('viewABinary');
|
|
534
504
|
});
|
|
535
505
|
|
|
536
506
|
// Switch to View B (completely different filter)
|
|
@@ -549,96 +519,28 @@ describe('useProfileFiltersUrlState', () => {
|
|
|
549
519
|
});
|
|
550
520
|
|
|
551
521
|
await waitFor(() => {
|
|
552
|
-
expect(
|
|
553
|
-
const
|
|
522
|
+
expect(onUrlUpdate).toHaveBeenCalled();
|
|
523
|
+
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0];
|
|
524
|
+
const filterValue = lastCall.searchParams.get('profile_filters');
|
|
554
525
|
|
|
555
526
|
// View A's filters should be completely gone
|
|
556
|
-
expect(
|
|
557
|
-
expect(
|
|
527
|
+
expect(filterValue).not.toContain('viewAFunc');
|
|
528
|
+
expect(filterValue).not.toContain('viewABinary');
|
|
558
529
|
|
|
559
530
|
// Only View B's filter should be present
|
|
560
|
-
expect(
|
|
561
|
-
});
|
|
562
|
-
});
|
|
563
|
-
|
|
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);
|
|
573
|
-
});
|
|
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');
|
|
531
|
+
expect(filterValue).toBe('f:fn:~:viewBOnly');
|
|
627
532
|
});
|
|
628
533
|
});
|
|
629
534
|
|
|
630
535
|
it('should not change filters when clicking the same view tab', async () => {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()});
|
|
536
|
+
const {result} = renderHook(() => useProfileFiltersUrlState(), {
|
|
537
|
+
wrapper: createWrapper({profile_filters: 's:fn:=:existingFilter'}),
|
|
538
|
+
});
|
|
635
539
|
|
|
636
540
|
await waitFor(() => {
|
|
637
541
|
expect(result.current.appliedFilters).toHaveLength(1);
|
|
638
542
|
});
|
|
639
543
|
|
|
640
|
-
mockNavigateTo.mockClear();
|
|
641
|
-
|
|
642
544
|
// Apply the same filters (simulating clicking the same view tab)
|
|
643
545
|
const sameFilters: ProfileFilter[] = [
|
|
644
546
|
{
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
import {useCallback, useMemo} from 'react';
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import {createParser, useQueryState} from 'nuqs';
|
|
17
|
+
|
|
17
18
|
import {safeDecode} from '@parca/utilities';
|
|
18
19
|
|
|
19
20
|
import {isPresetKey} from './filterPresets';
|
|
@@ -137,31 +138,32 @@ export const decodeProfileFilters = (encoded: string): ProfileFilter[] => {
|
|
|
137
138
|
}
|
|
138
139
|
};
|
|
139
140
|
|
|
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
|
+
|
|
140
149
|
export const useProfileFiltersUrlState = (): {
|
|
141
150
|
appliedFilters: ProfileFilter[];
|
|
142
|
-
setAppliedFilters:
|
|
151
|
+
setAppliedFilters: (filters: ProfileFilter[]) => void;
|
|
143
152
|
forceApplyFilters: (filters: ProfileFilter[]) => void;
|
|
144
153
|
} => {
|
|
145
|
-
const
|
|
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
|
-
);
|
|
154
|
+
const [appliedFilters, setRawFilters] = useQueryState('profile_filters', profileFiltersParser);
|
|
160
155
|
|
|
161
156
|
const memoizedAppliedFilters = useMemo(() => {
|
|
162
157
|
return appliedFilters ?? [];
|
|
163
158
|
}, [appliedFilters]);
|
|
164
159
|
|
|
160
|
+
const setAppliedFilters = useCallback(
|
|
161
|
+
(filters: ProfileFilter[]) => {
|
|
162
|
+
void setRawFilters(filters);
|
|
163
|
+
},
|
|
164
|
+
[setRawFilters]
|
|
165
|
+
);
|
|
166
|
+
|
|
165
167
|
// Force apply filters (bypasses preserve-existing strategy)
|
|
166
168
|
const forceApplyFilters = useCallback(
|
|
167
169
|
(filters: ProfileFilter[]) => {
|
|
@@ -172,11 +174,9 @@ export const useProfileFiltersUrlState = (): {
|
|
|
172
174
|
return f.value !== '' && f.type != null && f.field != null && f.matchType != null;
|
|
173
175
|
});
|
|
174
176
|
|
|
175
|
-
|
|
176
|
-
setAppliedFilters(validFilters);
|
|
177
|
-
});
|
|
177
|
+
setAppliedFilters(validFilters);
|
|
178
178
|
},
|
|
179
|
-
[
|
|
179
|
+
[setAppliedFilters]
|
|
180
180
|
);
|
|
181
181
|
|
|
182
182
|
return {
|