@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/InvertCallStack/index.tsx"],"names":[],"mappings":"
|
|
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 {
|
|
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 [
|
|
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
|
|
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:
|
|
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":"
|
|
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 {
|
|
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
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
5
|
-
import {
|
|
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
|
-
//
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
|
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(
|
|
237
|
-
const
|
|
238
|
-
expect(
|
|
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
|
-
|
|
243
|
-
const { result } = renderHook(() => useProfileFiltersUrlState(), {
|
|
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(
|
|
249
|
-
const
|
|
250
|
-
|
|
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
|
-
|
|
262
|
-
const { result } = renderHook(() => useProfileFiltersUrlState(), {
|
|
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(
|
|
281
|
-
const
|
|
282
|
-
expect(
|
|
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
|
-
|
|
287
|
-
const { result } = renderHook(() => useProfileFiltersUrlState(), {
|
|
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(
|
|
293
|
-
const
|
|
294
|
-
|
|
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
|
|
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(
|
|
314
|
-
const
|
|
315
|
-
expect(
|
|
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
|
|
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(
|
|
339
|
-
const
|
|
340
|
-
expect(
|
|
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
|
|
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(
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
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(
|
|
393
|
-
const
|
|
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(
|
|
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
|
-
|
|
413
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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(
|
|
452
|
-
const
|
|
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(
|
|
455
|
-
expect(
|
|
423
|
+
expect(filterValue).not.toContain('viewAFunc');
|
|
424
|
+
expect(filterValue).not.toContain('viewABinary');
|
|
456
425
|
// Only View B's filter should be present
|
|
457
|
-
expect(
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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;
|
|
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 {
|
|
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] =
|
|
77
|
-
|
|
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
|
|
91
|
-
const [compareAbsolute
|
|
92
|
-
const isCompareAbsolute = compareAbsolute
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
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 {
|
|
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] =
|
|
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);
|