@parca/profile 0.19.20 → 0.19.21

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 (85) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts.map +1 -1
  3. package/dist/ProfileExplorer/ProfileExplorerCompare.js +1 -3
  4. package/dist/ProfileExplorer/index.d.ts.map +1 -1
  5. package/dist/ProfileExplorer/index.js +5 -8
  6. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.d.ts.map +1 -1
  7. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.js +0 -2
  8. package/dist/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.d.ts +0 -1
  9. package/dist/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.d.ts.map +1 -1
  10. package/dist/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.js +2 -11
  11. package/dist/ProfileFlameGraph/FlameGraphArrow/index.d.ts.map +1 -1
  12. package/dist/ProfileFlameGraph/FlameGraphArrow/index.js +6 -14
  13. package/dist/ProfileSelector/useAutoQuerySelector.d.ts.map +1 -1
  14. package/dist/ProfileSelector/useAutoQuerySelector.js +1 -1
  15. package/dist/ProfileSource.d.ts +4 -11
  16. package/dist/ProfileSource.d.ts.map +1 -1
  17. package/dist/ProfileSource.js +6 -14
  18. package/dist/ProfileView/components/ColorStackLegend.d.ts.map +1 -1
  19. package/dist/ProfileView/components/ColorStackLegend.js +14 -10
  20. package/dist/ProfileView/components/DashboardItems/index.d.ts +1 -3
  21. package/dist/ProfileView/components/DashboardItems/index.d.ts.map +1 -1
  22. package/dist/ProfileView/components/DashboardItems/index.js +2 -2
  23. package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts.map +1 -1
  24. package/dist/ProfileView/components/GroupByLabelsDropdown/index.js +14 -1
  25. package/dist/ProfileView/components/InvertCallStack/index.js +1 -1
  26. package/dist/ProfileView/components/ProfileFilters/index.d.ts +5 -0
  27. package/dist/ProfileView/components/ProfileFilters/index.d.ts.map +1 -0
  28. package/dist/ProfileView/components/ProfileFilters/index.js +173 -0
  29. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts +17 -0
  30. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts.map +1 -0
  31. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.js +209 -0
  32. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts +8 -0
  33. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -0
  34. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +87 -0
  35. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts.map +1 -1
  36. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +3 -10
  37. package/dist/ProfileView/components/Toolbars/index.d.ts +0 -5
  38. package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
  39. package/dist/ProfileView/components/Toolbars/index.js +6 -6
  40. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.d.ts.map +1 -1
  41. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.js +3 -12
  42. package/dist/ProfileView/hooks/useVisualizationState.d.ts +0 -3
  43. package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
  44. package/dist/ProfileView/hooks/useVisualizationState.js +0 -7
  45. package/dist/ProfileView/index.d.ts.map +1 -1
  46. package/dist/ProfileView/index.js +3 -5
  47. package/dist/ProfileViewWithData.d.ts.map +1 -1
  48. package/dist/ProfileViewWithData.js +8 -7
  49. package/dist/Sandwich/index.d.ts.map +1 -1
  50. package/dist/Sandwich/index.js +4 -2
  51. package/dist/Table/index.d.ts +0 -2
  52. package/dist/Table/index.d.ts.map +1 -1
  53. package/dist/Table/index.js +5 -32
  54. package/dist/styles.css +1 -1
  55. package/dist/useQuery.d.ts +1 -1
  56. package/dist/useQuery.d.ts.map +1 -1
  57. package/dist/useQuery.js +7 -40
  58. package/package.json +7 -7
  59. package/src/ProfileExplorer/ProfileExplorerCompare.tsx +0 -4
  60. package/src/ProfileExplorer/index.tsx +4 -13
  61. package/src/ProfileFlameGraph/FlameGraphArrow/ContextMenu.tsx +0 -2
  62. package/src/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.tsx +1 -14
  63. package/src/ProfileFlameGraph/FlameGraphArrow/index.tsx +4 -16
  64. package/src/ProfileSelector/useAutoQuerySelector.ts +1 -2
  65. package/src/ProfileSource.tsx +6 -49
  66. package/src/ProfileView/components/ColorStackLegend.tsx +16 -12
  67. package/src/ProfileView/components/DashboardItems/index.tsx +0 -6
  68. package/src/ProfileView/components/GroupByLabelsDropdown/index.tsx +15 -2
  69. package/src/ProfileView/components/InvertCallStack/index.tsx +1 -1
  70. package/src/ProfileView/components/ProfileFilters/index.tsx +294 -0
  71. package/src/ProfileView/components/ProfileFilters/useProfileFilters.ts +284 -0
  72. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +103 -0
  73. package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +3 -16
  74. package/src/ProfileView/components/Toolbars/index.tsx +5 -35
  75. package/src/ProfileView/hooks/useResetStateOnProfileTypeChange.ts +5 -12
  76. package/src/ProfileView/hooks/useVisualizationState.ts +0 -11
  77. package/src/ProfileView/index.tsx +1 -15
  78. package/src/ProfileViewWithData.tsx +9 -9
  79. package/src/Sandwich/index.tsx +5 -2
  80. package/src/Table/index.tsx +3 -44
  81. package/src/useQuery.tsx +11 -43
  82. package/dist/ProfileView/components/FilterByFunctionButton.d.ts +0 -3
  83. package/dist/ProfileView/components/FilterByFunctionButton.d.ts.map +0 -1
  84. package/dist/ProfileView/components/FilterByFunctionButton.js +0 -89
  85. package/src/ProfileView/components/FilterByFunctionButton.tsx +0 -128
@@ -0,0 +1,294 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import {useCallback} from 'react';
15
+
16
+ import {Icon} from '@iconify/react';
17
+ import cx from 'classnames';
18
+
19
+ import {Button, Input, Select, type SelectItem} from '@parca/components';
20
+
21
+ import {useProfileFilters, type ProfileFilter} from './useProfileFilters';
22
+
23
+ export const isFilterComplete = (filter: ProfileFilter): boolean => {
24
+ return (
25
+ filter.value !== '' && filter.type != null && filter.field != null && filter.matchType != null
26
+ );
27
+ };
28
+
29
+ const filterTypeItems: SelectItem[] = [
30
+ {
31
+ key: 'stack',
32
+ element: {
33
+ active: <>Stack Filter</>,
34
+ expanded: (
35
+ <>
36
+ <span>Stack Filter</span>
37
+ <br />
38
+ <span className="text-xs">Filters entire call stacks</span>
39
+ </>
40
+ ),
41
+ },
42
+ },
43
+ {
44
+ key: 'frame',
45
+ element: {
46
+ active: <>Frame Filter</>,
47
+ expanded: (
48
+ <>
49
+ <span>Frame Filter</span>
50
+ <br />
51
+ <span className="text-xs">Filters individual frames</span>
52
+ </>
53
+ ),
54
+ },
55
+ },
56
+ ];
57
+
58
+ const fieldItems: SelectItem[] = [
59
+ {
60
+ key: 'function_name',
61
+ element: {
62
+ active: <>Function</>,
63
+ expanded: <>Function Name</>,
64
+ },
65
+ },
66
+ {
67
+ key: 'binary',
68
+ element: {
69
+ active: <>Binary</>,
70
+ expanded: <>Binary/Executable Name</>,
71
+ },
72
+ },
73
+ {
74
+ key: 'system_name',
75
+ element: {
76
+ active: <>System Name</>,
77
+ expanded: <>System Name</>,
78
+ },
79
+ },
80
+ {
81
+ key: 'filename',
82
+ element: {
83
+ active: <>Filename</>,
84
+ expanded: <>Source Filename</>,
85
+ },
86
+ },
87
+ {
88
+ key: 'address',
89
+ element: {
90
+ active: <>Address</>,
91
+ expanded: <>Memory Address</>,
92
+ },
93
+ },
94
+ {
95
+ key: 'line_number',
96
+ element: {
97
+ active: <>Line Number</>,
98
+ expanded: <>Source Line Number</>,
99
+ },
100
+ },
101
+ ];
102
+
103
+ const stringMatchTypeItems: SelectItem[] = [
104
+ {
105
+ key: 'equal',
106
+ element: {
107
+ active: <>Equals</>,
108
+ expanded: <>Equals</>,
109
+ },
110
+ },
111
+ {
112
+ key: 'not_equal',
113
+ element: {
114
+ active: <>Not Equals</>,
115
+ expanded: <>Not Equals</>,
116
+ },
117
+ },
118
+ {
119
+ key: 'contains',
120
+ element: {
121
+ active: <>Contains</>,
122
+ expanded: <>Contains</>,
123
+ },
124
+ },
125
+ {
126
+ key: 'not_contains',
127
+ element: {
128
+ active: <>Not Contains</>,
129
+ expanded: <>Not Contains</>,
130
+ },
131
+ },
132
+ ];
133
+
134
+ const numberMatchTypeItems: SelectItem[] = [
135
+ {
136
+ key: 'equal',
137
+ element: {
138
+ active: <>Equals</>,
139
+ expanded: <>Equals</>,
140
+ },
141
+ },
142
+ {
143
+ key: 'not_equal',
144
+ element: {
145
+ active: <>Not Equals</>,
146
+ expanded: <>Not Equals</>,
147
+ },
148
+ },
149
+ ];
150
+
151
+ const ProfileFilters = (): JSX.Element => {
152
+ const {
153
+ localFilters,
154
+ appliedFilters,
155
+ hasUnsavedChanges,
156
+ onApplyFilters,
157
+ addFilter,
158
+ removeFilter,
159
+ updateFilter,
160
+ resetFilters,
161
+ } = useProfileFilters();
162
+
163
+ const handleKeyDown = useCallback(
164
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
165
+ if (e.key === 'Enter') {
166
+ e.preventDefault();
167
+ if (e.currentTarget.value.trim() === '') {
168
+ return;
169
+ }
170
+ onApplyFilters();
171
+ }
172
+ },
173
+ [onApplyFilters]
174
+ );
175
+
176
+ const filtersToRender = localFilters.length > 0 ? localFilters : appliedFilters ?? [];
177
+
178
+ return (
179
+ <div className="flex gap-2 w-full">
180
+ <div className="flex-1 flex flex-wrap gap-2">
181
+ {filtersToRender.map(filter => {
182
+ const isNumberField = filter.field === 'address' || filter.field === 'line_number';
183
+ const matchTypeItems = isNumberField ? numberMatchTypeItems : stringMatchTypeItems;
184
+
185
+ return (
186
+ <div key={filter.id} className="flex items-center gap-0">
187
+ <Select
188
+ items={filterTypeItems}
189
+ selectedKey={filter.type}
190
+ placeholder="Select Filter"
191
+ onSelection={key => {
192
+ const newType = key as 'stack' | 'frame';
193
+ updateFilter(filter.id, {
194
+ type: newType,
195
+ field: filter.field ?? 'function_name',
196
+ matchType: filter.matchType ?? 'contains',
197
+ });
198
+ }}
199
+ className={cx(
200
+ 'rounded-l-md pr-1 gap-0 focus:z-50 focus:relative focus:outline-1 rounded-r-none ',
201
+ filter.type != null ? 'border-r-0 w-28' : 'w-32'
202
+ )}
203
+ />
204
+
205
+ {filter.type != null && (
206
+ <>
207
+ <Select
208
+ items={fieldItems}
209
+ selectedKey={filter.field ?? ''}
210
+ onSelection={key => {
211
+ const newField = key as ProfileFilter['field'];
212
+ const isNewFieldNumber = newField === 'address' || newField === 'line_number';
213
+ const isCurrentFieldNumber =
214
+ filter.field === 'address' || filter.field === 'line_number';
215
+
216
+ if (isNewFieldNumber !== isCurrentFieldNumber) {
217
+ updateFilter(filter.id, {
218
+ field: newField,
219
+ matchType: 'equal',
220
+ });
221
+ } else {
222
+ updateFilter(filter.id, {field: newField});
223
+ }
224
+ }}
225
+ className="rounded-none border-r-0 w-32 pr-1 gap-0 focus:z-50 focus:relative focus:outline-1"
226
+ />
227
+
228
+ <Select
229
+ items={matchTypeItems}
230
+ selectedKey={filter.matchType ?? ''}
231
+ onSelection={key =>
232
+ updateFilter(filter.id, {matchType: key as ProfileFilter['matchType']})
233
+ }
234
+ className="rounded-none border-r-0 pr-1 gap-0 focus:z-50 focus:relative focus:outline-1"
235
+ />
236
+
237
+ <Input
238
+ placeholder="Value"
239
+ value={filter.value}
240
+ onChange={e => updateFilter(filter.id, {value: e.target.value})}
241
+ onKeyDown={handleKeyDown}
242
+ className="rounded-none w-36 text-sm focus:outline-1"
243
+ />
244
+ </>
245
+ )}
246
+
247
+ <Button
248
+ variant="neutral"
249
+ onClick={() => {
250
+ if (localFilters.length === 1) {
251
+ resetFilters();
252
+ } else {
253
+ removeFilter(filter.id);
254
+ }
255
+ }}
256
+ className={cx(
257
+ 'h-[38px] p-3',
258
+ filter.type != null ? 'rounded-none rounded-r-md' : 'rounded-l-none rounded-r-md'
259
+ )}
260
+ >
261
+ <Icon icon="mdi:close" className="h-4 w-4" />
262
+ </Button>
263
+ </div>
264
+ );
265
+ })}
266
+
267
+ {localFilters.length > 0 && (
268
+ <Button variant="neutral" onClick={addFilter} className="p-3 h-[38px]">
269
+ <Icon icon="mdi:filter-plus-outline" className="h-4 w-4" />
270
+ </Button>
271
+ )}
272
+
273
+ {localFilters.length === 0 && (appliedFilters?.length ?? 0) === 0 && (
274
+ <Button variant="neutral" onClick={addFilter} className="flex items-center gap-2">
275
+ <Icon icon="mdi:filter-outline" className="h-4 w-4" />
276
+ <span>Filter</span>
277
+ </Button>
278
+ )}
279
+ </div>
280
+
281
+ {localFilters.length > 0 && hasUnsavedChanges && localFilters.some(isFilterComplete) && (
282
+ <Button
283
+ variant="primary"
284
+ onClick={onApplyFilters}
285
+ className={cx('flex items-center gap-2 self-end')}
286
+ >
287
+ <span>Apply</span>
288
+ </Button>
289
+ )}
290
+ </div>
291
+ );
292
+ };
293
+
294
+ export default ProfileFilters;
@@ -0,0 +1,284 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import {useCallback, useEffect, useMemo} from 'react';
15
+
16
+ import {type Filter} from '@parca/client';
17
+ import {
18
+ selectLocalFilters,
19
+ setLocalFilters,
20
+ useAppDispatch,
21
+ useAppSelector,
22
+ type ProfileFilter,
23
+ } from '@parca/store';
24
+
25
+ import {useProfileFiltersUrlState} from './useProfileFiltersUrlState';
26
+
27
+ export type {ProfileFilter};
28
+
29
+ // Convert ProfileFilter[] to protobuf Filter[] matching the expected structure
30
+ const convertToProtoFilters = (profileFilters: ProfileFilter[]): Filter[] => {
31
+ return profileFilters
32
+ .filter(f => f.value !== '' && f.type != null && f.field != null && f.matchType != null) // Only include complete filters with values
33
+ .map(f => {
34
+ // Build the condition based on field type
35
+ const isNumberField = f.field === 'address' || f.field === 'line_number';
36
+
37
+ let condition: any;
38
+ if (isNumberField) {
39
+ const numValue = BigInt(f.value);
40
+ condition = {
41
+ condition:
42
+ f.matchType === 'equal'
43
+ ? {oneofKind: 'equal' as const, equal: numValue}
44
+ : {oneofKind: 'notEqual' as const, notEqual: numValue},
45
+ };
46
+ } else {
47
+ condition = {
48
+ condition:
49
+ f.matchType === 'equal'
50
+ ? {oneofKind: 'equal' as const, equal: f.value}
51
+ : f.matchType === 'not_equal'
52
+ ? {oneofKind: 'notEqual' as const, notEqual: f.value}
53
+ : f.matchType === 'contains'
54
+ ? {oneofKind: 'contains' as const, contains: f.value}
55
+ : {oneofKind: 'notContains' as const, notContains: f.value},
56
+ };
57
+ }
58
+
59
+ // Create FilterCriteria
60
+ const criteria: any = {};
61
+ switch (f.field) {
62
+ case 'function_name':
63
+ criteria.functionName = condition;
64
+ break;
65
+ case 'binary':
66
+ criteria.binary = condition;
67
+ break;
68
+ case 'system_name':
69
+ criteria.systemName = condition;
70
+ break;
71
+ case 'filename':
72
+ criteria.filename = condition;
73
+ break;
74
+ case 'address':
75
+ criteria.address = condition;
76
+ break;
77
+ case 'line_number':
78
+ criteria.lineNumber = condition;
79
+ break;
80
+ }
81
+
82
+ // Create the appropriate filter type with proper oneofKind structure
83
+ if (f.type === 'stack') {
84
+ return {
85
+ filter: {
86
+ oneofKind: 'stackFilter' as const,
87
+ stackFilter: {
88
+ filter: {
89
+ oneofKind: 'criteria' as const,
90
+ criteria,
91
+ },
92
+ },
93
+ },
94
+ };
95
+ } else {
96
+ return {
97
+ filter: {
98
+ oneofKind: 'frameFilter' as const,
99
+ frameFilter: {
100
+ filter: {
101
+ oneofKind: 'criteria' as const,
102
+ criteria,
103
+ },
104
+ },
105
+ },
106
+ };
107
+ }
108
+ });
109
+ };
110
+
111
+ export const useProfileFilters = (): {
112
+ localFilters: ProfileFilter[];
113
+ appliedFilters: ProfileFilter[];
114
+ protoFilters: Filter[];
115
+ hasUnsavedChanges: boolean;
116
+ onApplyFilters: () => void;
117
+ addFilter: () => void;
118
+ excludeBinary: (binaryName: string) => void;
119
+ removeExcludeBinary: (binaryName: string) => void;
120
+ removeFilter: (id: string) => void;
121
+ updateFilter: (id: string, updates: Partial<ProfileFilter>) => void;
122
+ resetFilters: () => void;
123
+ } => {
124
+ const {appliedFilters, setAppliedFilters} = useProfileFiltersUrlState();
125
+ const dispatch = useAppDispatch();
126
+ const localFilters = useAppSelector(selectLocalFilters);
127
+
128
+ useEffect(() => {
129
+ if (appliedFilters != null && appliedFilters.length > 0) {
130
+ // Check if they're different to avoid unnecessary updates
131
+ const areFiltersEqual =
132
+ appliedFilters.length === localFilters.length &&
133
+ appliedFilters.every((applied, index) => {
134
+ const local = localFilters[index];
135
+ return (
136
+ local != null &&
137
+ applied.type === local.type &&
138
+ applied.field === local.field &&
139
+ applied.matchType === local.matchType &&
140
+ applied.value === local.value
141
+ );
142
+ });
143
+
144
+ if (!areFiltersEqual) {
145
+ dispatch(setLocalFilters(appliedFilters));
146
+ }
147
+ } else if (appliedFilters != null && appliedFilters.length === 0 && localFilters.length > 0) {
148
+ dispatch(setLocalFilters([]));
149
+ }
150
+ // eslint-disable-next-line react-hooks/exhaustive-deps
151
+ }, []);
152
+
153
+ const hasUnsavedChanges = useMemo(() => {
154
+ const localWithValues = localFilters.filter(f => f.value !== '');
155
+ const appliedWithValues = (appliedFilters ?? []).filter(f => f.value !== '');
156
+
157
+ if (localWithValues.length !== appliedWithValues.length) return true;
158
+
159
+ return !localWithValues.every((local, index) => {
160
+ const applied = appliedWithValues[index];
161
+ return (
162
+ local.type === applied?.type &&
163
+ local.field === applied?.field &&
164
+ local.matchType === applied?.matchType &&
165
+ local.value === applied?.value
166
+ );
167
+ });
168
+ }, [localFilters, appliedFilters]);
169
+
170
+ const addFilter = useCallback(() => {
171
+ const newFilter: ProfileFilter = {
172
+ id: `filter-${Date.now()}-${Math.random()}`,
173
+ value: '',
174
+ };
175
+ dispatch(setLocalFilters([...localFilters, newFilter]));
176
+ }, [dispatch, localFilters]);
177
+
178
+ const excludeBinary = useCallback(
179
+ (binaryName: string) => {
180
+ // Check if this binary is already being filtered with not_contains
181
+ const existingFilter = (appliedFilters ?? []).find(
182
+ f =>
183
+ f.type === 'frame' &&
184
+ f.field === 'binary' &&
185
+ f.matchType === 'not_contains' &&
186
+ f.value === binaryName
187
+ );
188
+
189
+ if (existingFilter != null) {
190
+ return; // Already exists, don't add duplicate
191
+ }
192
+
193
+ const newFilter: ProfileFilter = {
194
+ id: `filter-${Date.now()}-${Math.random()}`,
195
+ type: 'frame',
196
+ field: 'binary',
197
+ matchType: 'not_contains',
198
+ value: binaryName,
199
+ };
200
+ dispatch(setLocalFilters([...localFilters, newFilter]));
201
+
202
+ // Auto-apply the filter since it has a value
203
+ const filtersToApply = [...(appliedFilters ?? []), newFilter];
204
+ setAppliedFilters(filtersToApply);
205
+ },
206
+ [appliedFilters, setAppliedFilters, dispatch, localFilters]
207
+ );
208
+
209
+ const removeExcludeBinary = useCallback(
210
+ (binaryName: string) => {
211
+ // Search for the exclude filter (not_contains) for this binary
212
+ const filterToRemove = (appliedFilters ?? []).find(
213
+ f =>
214
+ f.type === 'frame' &&
215
+ f.field === 'binary' &&
216
+ f.matchType === 'not_contains' &&
217
+ f.value === binaryName
218
+ );
219
+
220
+ if (filterToRemove != null) {
221
+ // Remove the filter from applied filters
222
+ const updatedAppliedFilters = (appliedFilters ?? []).filter(
223
+ f => f.id !== filterToRemove.id
224
+ );
225
+ setAppliedFilters(updatedAppliedFilters);
226
+
227
+ // Also remove from local filters
228
+ const updatedLocalFilters = localFilters.filter(f => f.id !== filterToRemove.id);
229
+ dispatch(setLocalFilters(updatedLocalFilters));
230
+ }
231
+ },
232
+ [appliedFilters, setAppliedFilters, dispatch, localFilters]
233
+ );
234
+
235
+ const removeFilter = useCallback(
236
+ (id: string) => {
237
+ dispatch(setLocalFilters(localFilters.filter(f => f.id !== id)));
238
+ },
239
+ [dispatch, localFilters]
240
+ );
241
+
242
+ const updateFilter = useCallback(
243
+ (id: string, updates: Partial<ProfileFilter>) => {
244
+ dispatch(setLocalFilters(localFilters.map(f => (f.id === id ? {...f, ...updates} : f))));
245
+ },
246
+ [dispatch, localFilters]
247
+ );
248
+
249
+ const resetFilters = useCallback(() => {
250
+ dispatch(setLocalFilters([]));
251
+ setAppliedFilters([]);
252
+ }, [dispatch, setAppliedFilters]);
253
+
254
+ const onApplyFilters = useCallback((): void => {
255
+ const validFilters = localFilters.filter(
256
+ f => f.value !== '' && f.type != null && f.field != null && f.matchType != null
257
+ );
258
+
259
+ const filtersToApply = validFilters.map((f, index) => ({
260
+ ...f,
261
+ id: `filter-${Date.now()}-${index}`,
262
+ }));
263
+
264
+ setAppliedFilters(filtersToApply);
265
+ }, [localFilters, setAppliedFilters]);
266
+
267
+ const protoFilters = useMemo(() => {
268
+ return convertToProtoFilters(appliedFilters ?? []);
269
+ }, [appliedFilters]);
270
+
271
+ return {
272
+ localFilters,
273
+ appliedFilters,
274
+ protoFilters,
275
+ hasUnsavedChanges,
276
+ onApplyFilters,
277
+ addFilter,
278
+ excludeBinary,
279
+ removeExcludeBinary,
280
+ removeFilter,
281
+ updateFilter,
282
+ resetFilters,
283
+ };
284
+ };
@@ -0,0 +1,103 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import {useURLStateCustom, type ParamValueSetterCustom} from '@parca/components';
15
+ import {type ProfileFilter} from '@parca/store';
16
+
17
+ // Compact encoding mappings
18
+ const TYPE_MAP: Record<string, string> = {
19
+ stack: 's',
20
+ frame: 'f',
21
+ };
22
+
23
+ const FIELD_MAP: Record<string, string> = {
24
+ function_name: 'fn',
25
+ binary: 'b',
26
+ system_name: 'sn',
27
+ filename: 'f',
28
+ address: 'a',
29
+ line_number: 'ln',
30
+ };
31
+
32
+ const MATCH_MAP: Record<string, string> = {
33
+ equal: '=',
34
+ not_equal: '!=',
35
+ contains: '~',
36
+ not_contains: '!~',
37
+ };
38
+
39
+ // Reverse mappings for decoding
40
+ const TYPE_MAP_REVERSE = Object.fromEntries(Object.entries(TYPE_MAP).map(([k, v]) => [v, k]));
41
+ const FIELD_MAP_REVERSE = Object.fromEntries(Object.entries(FIELD_MAP).map(([k, v]) => [v, k]));
42
+ const MATCH_MAP_REVERSE = Object.fromEntries(Object.entries(MATCH_MAP).map(([k, v]) => [v, k]));
43
+
44
+ // Encode filters to compact string format
45
+ const encodeProfileFilters = (filters: ProfileFilter[]): string => {
46
+ if (filters.length === 0) return '';
47
+
48
+ return filters
49
+ .filter(f => f.value !== '' && f.type != null && f.field != null && f.matchType != null)
50
+ .map(f => {
51
+ const type = TYPE_MAP[f.type!];
52
+ const field = FIELD_MAP[f.field!];
53
+ const match = MATCH_MAP[f.matchType!];
54
+ const value = encodeURIComponent(f.value);
55
+ return `${type}:${field}:${match}:${value}`;
56
+ })
57
+ .join(',');
58
+ };
59
+
60
+ // Decode filters from compact string format
61
+ export const decodeProfileFilters = (encoded: string): ProfileFilter[] => {
62
+ if (encoded === '' || encoded === undefined) return [];
63
+
64
+ try {
65
+ return encoded.split(',').map((filter, index) => {
66
+ const [type, field, match, ...valueParts] = filter.split(':');
67
+ const value = decodeURIComponent(valueParts.join(':')); // Handle values with colons
68
+
69
+ return {
70
+ id: `filter-${Date.now()}-${index}`,
71
+ type: TYPE_MAP_REVERSE[type] as ProfileFilter['type'],
72
+ field: FIELD_MAP_REVERSE[field] as ProfileFilter['field'],
73
+ matchType: MATCH_MAP_REVERSE[match] as ProfileFilter['matchType'],
74
+ value,
75
+ };
76
+ });
77
+ } catch {
78
+ return [];
79
+ }
80
+ };
81
+
82
+ export const useProfileFiltersUrlState = (): {
83
+ appliedFilters: ProfileFilter[];
84
+ setAppliedFilters: ParamValueSetterCustom<ProfileFilter[]>;
85
+ } => {
86
+ // Store applied filters in URL state for persistence using compact encoding
87
+ const [appliedFilters, setAppliedFilters] = useURLStateCustom<ProfileFilter[]>(
88
+ 'profile_filters',
89
+ {
90
+ parse: value => {
91
+ return decodeProfileFilters(value as string);
92
+ },
93
+ stringify: value => {
94
+ return encodeProfileFilters(value);
95
+ },
96
+ }
97
+ );
98
+
99
+ return {
100
+ appliedFilters,
101
+ setAppliedFilters,
102
+ };
103
+ };